gammasimtools 0.24.0__py3-none-any.whl → 0.26.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.24.0.dist-info → gammasimtools-0.26.0.dist-info}/METADATA +2 -1
  2. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/RECORD +134 -130
  3. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/entry_points.txt +3 -1
  4. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/licenses/LICENSE +1 -1
  5. simtools/_version.py +2 -2
  6. simtools/application_control.py +78 -0
  7. simtools/applications/calculate_incident_angles.py +0 -2
  8. simtools/applications/convert_geo_coordinates_of_array_elements.py +1 -2
  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 +2 -6
  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/derive_mirror_rnda.py +1 -3
  20. simtools/applications/derive_psf_parameters.py +5 -1
  21. simtools/applications/derive_pulse_shape_parameters.py +194 -0
  22. simtools/applications/derive_trigger_rates.py +1 -1
  23. simtools/applications/docs_produce_array_element_report.py +2 -8
  24. simtools/applications/docs_produce_calibration_reports.py +1 -3
  25. simtools/applications/docs_produce_model_parameter_reports.py +0 -2
  26. simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
  27. simtools/applications/generate_array_config.py +0 -1
  28. simtools/applications/generate_corsika_histograms.py +48 -235
  29. simtools/applications/generate_regular_arrays.py +5 -35
  30. simtools/applications/generate_simtel_event_data.py +2 -2
  31. simtools/applications/maintain_simulation_model_add_production.py +2 -2
  32. simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
  33. simtools/applications/plot_array_layout.py +64 -108
  34. simtools/applications/plot_simulated_event_distributions.py +57 -0
  35. simtools/applications/plot_tabular_data.py +0 -1
  36. simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
  37. simtools/applications/production_derive_corsika_limits.py +1 -1
  38. simtools/applications/production_generate_grid.py +0 -1
  39. simtools/applications/run_application.py +1 -1
  40. simtools/applications/simulate_flasher.py +3 -4
  41. simtools/applications/simulate_illuminator.py +0 -1
  42. simtools/applications/simulate_pedestals.py +2 -6
  43. simtools/applications/simulate_prod.py +9 -28
  44. simtools/applications/simulate_prod_htcondor_generator.py +8 -1
  45. simtools/applications/submit_array_layouts.py +7 -7
  46. simtools/applications/submit_model_parameter_from_external.py +1 -3
  47. simtools/applications/validate_camera_efficiency.py +0 -1
  48. simtools/applications/validate_camera_fov.py +0 -1
  49. simtools/applications/validate_cumulative_psf.py +0 -2
  50. simtools/applications/validate_file_using_schema.py +49 -123
  51. simtools/applications/validate_optics.py +0 -13
  52. simtools/camera/camera_efficiency.py +1 -6
  53. simtools/camera/single_photon_electron_spectrum.py +2 -1
  54. simtools/configuration/commandline_parser.py +43 -8
  55. simtools/configuration/configurator.py +6 -11
  56. simtools/corsika/corsika_config.py +204 -99
  57. simtools/corsika/corsika_histograms.py +411 -1735
  58. simtools/corsika/primary_particle.py +1 -1
  59. simtools/data_model/metadata_collector.py +5 -2
  60. simtools/data_model/metadata_model.py +0 -4
  61. simtools/data_model/model_data_writer.py +27 -17
  62. simtools/data_model/schema.py +112 -5
  63. simtools/data_model/validate_data.py +80 -48
  64. simtools/db/db_handler.py +19 -8
  65. simtools/db/db_model_upload.py +2 -1
  66. simtools/db/mongo_db.py +133 -42
  67. simtools/dependencies.py +83 -44
  68. simtools/io/ascii_handler.py +4 -2
  69. simtools/io/table_handler.py +1 -1
  70. simtools/job_execution/htcondor_script_generator.py +0 -2
  71. simtools/layout/array_layout.py +4 -12
  72. simtools/layout/array_layout_utils.py +227 -58
  73. simtools/model/array_model.py +37 -18
  74. simtools/model/calibration_model.py +0 -4
  75. simtools/model/legacy_model_parameter.py +134 -0
  76. simtools/model/model_parameter.py +24 -14
  77. simtools/model/model_repository.py +18 -5
  78. simtools/model/model_utils.py +1 -6
  79. simtools/model/site_model.py +0 -4
  80. simtools/model/telescope_model.py +6 -11
  81. simtools/production_configuration/derive_corsika_limits.py +6 -11
  82. simtools/production_configuration/interpolation_handler.py +16 -16
  83. simtools/ray_tracing/incident_angles.py +5 -11
  84. simtools/ray_tracing/mirror_panel_psf.py +3 -7
  85. simtools/ray_tracing/psf_analysis.py +29 -27
  86. simtools/ray_tracing/psf_parameter_optimisation.py +822 -680
  87. simtools/ray_tracing/ray_tracing.py +6 -15
  88. simtools/reporting/docs_auto_report_generator.py +8 -13
  89. simtools/reporting/docs_read_parameters.py +70 -16
  90. simtools/runners/corsika_runner.py +15 -10
  91. simtools/runners/corsika_simtel_runner.py +9 -8
  92. simtools/runners/runner_services.py +17 -7
  93. simtools/runners/simtel_runner.py +11 -58
  94. simtools/runners/simtools_runner.py +2 -4
  95. simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
  96. simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
  97. simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
  98. simtools/schemas/simulation_models_info.schema.yml +2 -0
  99. simtools/settings.py +154 -0
  100. simtools/sim_events/file_info.py +128 -0
  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 +273 -0
  105. simtools/simtel/simtel_config_writer.py +146 -22
  106. simtools/simtel/simtel_table_reader.py +6 -4
  107. simtools/simtel/simulator_array.py +62 -23
  108. simtools/simtel/simulator_camera_efficiency.py +4 -6
  109. simtools/simtel/simulator_light_emission.py +101 -19
  110. simtools/simtel/simulator_ray_tracing.py +4 -10
  111. simtools/simulator.py +360 -353
  112. simtools/telescope_trigger_rates.py +3 -4
  113. simtools/testing/assertions.py +115 -8
  114. simtools/testing/configuration.py +2 -3
  115. simtools/testing/helpers.py +2 -3
  116. simtools/testing/log_inspector.py +5 -1
  117. simtools/testing/sim_telarray_metadata.py +1 -1
  118. simtools/testing/validate_output.py +69 -23
  119. simtools/utils/general.py +37 -0
  120. simtools/utils/geometry.py +0 -77
  121. simtools/utils/names.py +7 -9
  122. simtools/version.py +37 -0
  123. simtools/visualization/legend_handlers.py +21 -10
  124. simtools/visualization/plot_array_layout.py +312 -41
  125. simtools/visualization/plot_corsika_histograms.py +143 -605
  126. simtools/visualization/plot_mirrors.py +834 -0
  127. simtools/visualization/plot_pixels.py +2 -4
  128. simtools/visualization/plot_psf.py +0 -1
  129. simtools/visualization/plot_simtel_event_histograms.py +4 -4
  130. simtools/visualization/plot_simtel_events.py +6 -11
  131. simtools/visualization/plot_tables.py +8 -19
  132. simtools/visualization/visualize.py +22 -2
  133. simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
  134. simtools/applications/print_version.py +0 -53
  135. simtools/io/hdf5_handler.py +0 -139
  136. simtools/simtel/simtel_io_file_info.py +0 -62
  137. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/WHEEL +0 -0
  138. {gammasimtools-0.24.0.dist-info → gammasimtools-0.26.0.dist-info}/top_level.txt +0 -0
@@ -1,52 +1,26 @@
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
3
  import functools
4
4
  import logging
5
5
  import operator
6
- import re
7
- import time
8
6
  from pathlib import Path
9
7
 
10
8
  import boost_histogram as bh
11
9
  import numpy as np
12
10
  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
11
  from eventio import IACTFile
18
12
 
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."""
13
+ from simtools.utils.geometry import rotate
34
14
 
35
15
 
36
16
  class CorsikaHistograms:
37
17
  """
38
- Extracts the Cherenkov photons information from a CORSIKA IACT file.
18
+ Extract Cherenkov photons from a CORSIKA IACT file and fill histograms.
39
19
 
40
20
  Parameters
41
21
  ----------
42
22
  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.
23
+ CORSIKA IACT file.
50
24
 
51
25
  Raises
52
26
  ------
@@ -54,569 +28,106 @@ class CorsikaHistograms:
54
28
  if the input file given does not exist.
55
29
  """
56
30
 
57
- def __init__(self, input_file, label=None, output_path=None, hdf5_file_name=None):
58
- self.label = label
31
+ def __init__(self, input_file):
59
32
  self._logger = logging.getLogger(__name__)
60
33
  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
142
-
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,
172
- )
173
-
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 = {}
253
-
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]
257
- )
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
-
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.
36
+ raise FileNotFoundError(f"File {self.input_file} does not exist.")
283
37
 
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]
294
- )
295
-
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
305
-
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):
319
- """
320
- Return the dictionary with astropy units from the given list of parameters.
321
-
322
- Parameters
323
- ----------
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
38
+ self.events = None
39
+ self.hist = self._set_2d_distributions()
40
+ self.hist.update(self._set_1d_distributions())
347
41
 
348
- @property
349
- def telescope_indices(self):
42
+ def fill(self):
350
43
  """
351
- The telescope index (or indices), which are considered for the production of the histograms.
44
+ Fill Cherenkov photons histograms.
352
45
 
353
46
  Returns
354
47
  -------
355
- 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.
48
+ list: list of boost_histogram.Histogram instances.
374
49
 
375
50
  Raises
376
51
  ------
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.
52
+ AttributeError:
53
+ if event has not photon saved.
401
54
  """
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
55
+ self._read_event_headers()
410
56
 
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).
57
+ with IACTFile(self.input_file) as f:
58
+ telescope_positions = np.array(f.telescope_positions)
59
+ for event_counter, event in enumerate(f):
60
+ if hasattr(event, "photon_bunches"):
61
+ photons = list(event.photon_bunches.values())
62
+ self._fill_histograms(photons, event_counter, telescope_positions, True)
63
+
64
+ self._update_distributions()
65
+
66
+ def _read_event_headers(self):
67
+ """Read event information from headers."""
68
+ event_dtype = np.dtype(
69
+ [
70
+ ("particle_id", "i4"),
71
+ ("total_energy", "f8"),
72
+ ("azimuth_deg", "f8"),
73
+ ("zenith_deg", "f8"),
74
+ ("num_photons", "f8"),
75
+ ]
76
+ )
415
77
 
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.
78
+ with IACTFile(self.input_file) as iact_file:
79
+ records = [
80
+ (
81
+ event.header["particle_id"],
82
+ event.header["total_energy"],
83
+ np.rad2deg(event.header["azimuth"]),
84
+ np.rad2deg(event.header["zenith"]),
85
+ 0.0, # filled later when reading photon bunches
86
+ )
87
+ for event in iact_file
88
+ ]
418
89
 
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
90
+ self.events = np.array(records, dtype=event_dtype)
431
91
 
432
- def hist_config_to_yaml(self, file_name=None):
92
+ def _create_regular_axes(self, hist, axes):
433
93
  """
434
- Save the histogram configuration dictionary to a yaml file.
94
+ Create regular axis for a single histogram.
435
95
 
436
96
  Parameters
437
97
  ----------
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.
98
+ hist: dict
99
+ Histogram dictionary.
100
+ axes: list
101
+ List of axis names (e.g. ["x_bins", "y_bins"]).
467
102
 
468
103
  Returns
469
104
  -------
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.
105
+ list:
106
+ List of boost_histogram axis instances.
544
107
  """
545
108
  transform = {"log": bh.axis.transform.log, "linear": None}
546
109
 
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
110
  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"]
111
+ for axis in axes:
112
+ bins, start, stop = hist[axis][:3]
113
+ scale = hist[axis][3] if len(hist[axis]) > 3 else "linear"
114
+ if isinstance(start, u.quantity.Quantity):
115
+ start, stop = start.value, stop.value
564
116
  boost_axes.append(
565
117
  bh.axis.Regular(
566
- bins=self.hist_config[label][axis]["bins"],
118
+ bins=bins,
567
119
  start=start,
568
120
  stop=stop,
569
- transform=transform[self.hist_config[label][axis]["scale"]],
121
+ transform=transform[scale],
570
122
  )
571
123
  )
572
124
  return boost_axes
573
125
 
574
- def _create_histograms(self, individual_telescopes=False):
575
- """
576
- Create the histogram instances.
577
-
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):
126
+ def _fill_histograms(self, photons, event_counter, telescope_positions, rotate_photons=True):
615
127
  """
616
- Fill histograms with the information of the photons on the ground.
128
+ Fill Cherenkov photon histograms.
617
129
 
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.
130
+ For rotate_photons, the Cherenkov photon's coordinates are filled in the shower plane.
620
131
 
621
132
  Parameters
622
133
  ----------
@@ -630,1240 +141,405 @@ class CorsikaHistograms:
630
141
  incoming direction and the x axis,
631
142
  cy: direction cosine in the y direction, i.e., the cosine of the angle between the
632
143
  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.
144
+ time: time of arrival of the photon in ns.
636
145
  zem: altitude where the photon was generated in cm,
637
146
  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,
147
+ event_counter: int
148
+ Event counter.
149
+ telescope_positions: numpy.array
150
+ Array with the telescope positions with shape (M, 2), where M is the number of
151
+ telescopes in the array. The two columns are the x and y positions of the telescopes
152
+ in the CORSIKA coordinate system.
153
+ rotate_photons: bool
154
+ If True, the photon's coordinates are rotated to the plane perpendicular to the
155
+ incoming direction of the primary particle.
156
+ """
157
+ hist_str = "histogram"
158
+ for photon, telescope in zip(photons, telescope_positions):
159
+ if rotate_photons:
160
+ px, py = rotate(
161
+ photon["x"],
162
+ photon["y"],
163
+ self.events["azimuth_deg"][event_counter],
164
+ self.events["zenith_deg"][event_counter],
668
165
  )
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,
680
- )
681
-
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)
166
+ else:
167
+ px, py = photon["x"], photon["y"]
168
+
169
+ px -= telescope["x"]
170
+ py -= telescope["y"]
171
+ w = photon["photons"]
172
+
173
+ pxm = px * u.cm.to(u.m)
174
+ pym = py * u.cm.to(u.m)
175
+ zem = (photon["zem"] * u.cm).to(u.km)
176
+
177
+ self.hist["counts_xy"][hist_str].fill(pxm, pym, weight=w)
178
+ self.hist["density_xy"][hist_str].fill(pxm, pym, weight=w)
179
+ self.hist["direction_xy"][hist_str].fill(photon["cx"], photon["cy"], weight=w)
180
+ self.hist["time_altitude"][hist_str].fill(photon["time"] * u.ns, zem, weight=w)
181
+ self.hist["wavelength_altitude"][hist_str].fill(
182
+ np.abs(photon["wavelength"]) * u.nm, zem, weight=w
685
183
  )
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
-
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
184
 
769
- def _raise_if_no_histogram(self):
770
- """
771
- Raise an error if the histograms were not created.
185
+ r = np.hypot(px, py) * u.cm.to(u.m)
186
+ self.hist["counts_r"][hist_str].fill(r, weight=w)
187
+ self.hist["density_r"][hist_str].fill(r, weight=w)
772
188
 
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
189
+ self.events["num_photons"][event_counter] += np.sum(w)
786
190
 
787
- def _get_hist_2d_projection(self, label):
191
+ def get_hist_2d_projection(self, hist):
788
192
  """
789
193
  Get 2D distributions.
790
194
 
791
195
  Parameters
792
196
  ----------
793
- label: str
794
- Label to indicate which histogram.
197
+ hist: boost_histogram.Histogram
198
+ Histogram.
795
199
 
796
200
  Returns
797
201
  -------
798
202
  numpy.ndarray
799
- The counts of the histogram.
203
+ Histogram counts.
800
204
  numpy.array
801
- The x bin edges of the histograms.
205
+ Histogram x bin edges.
802
206
  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
207
+ Histogram y bin edges.
208
+ numpy.ndarray or None
209
+ Histogram uncertainties (sqrt of variance) if available.
210
+ """
211
+ view = hist.view()
212
+ if self._check_for_all_attributes(view):
213
+ counts = np.asarray([view["value"].T])
214
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
215
+ else:
216
+ counts = np.asarray([view.T])
217
+ uncertainties = None
218
+
219
+ x_edges = np.asarray([hist.axes.edges[0].flatten()])
220
+ y_edges = np.asarray([hist.axes.edges[1].flatten()])
221
+
222
+ return counts, x_edges, y_edges, uncertainties
223
+
224
+ def _get_hist_1d_from_numpy(self, label, hist):
225
+ """Get 1D histogram from numpy histogram."""
226
+ bins = hist["x_bins"][0]
227
+ start = hist["x_bins"][1] if hist["x_bins"][1] else np.min(self.events[label])
228
+ stop = hist["x_bins"][2] if hist["x_bins"][2] is not None else np.max(self.events[label])
229
+ scale = hist["x_bins"][3] if len(hist["x_bins"]) > 3 else "linear"
230
+ if scale == "log":
231
+ bin_edges = np.logspace(np.log10(start), np.log10(stop), bins + 1)
232
+ else:
233
+ bin_edges = np.linspace(start, stop, bins + 1)
234
+ histo_1d, _ = np.histogram(self.events[label], bins=bin_edges)
235
+ uncertainties = np.sqrt(histo_1d)
236
+ return (
237
+ histo_1d.reshape(1, bins),
238
+ bin_edges.reshape(1, bins + 1),
239
+ uncertainties.reshape(1, bins),
818
240
  )
819
241
 
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):
242
+ def get_hist_1d_projection(self, label, hist):
843
243
  """
844
- Get 2D histograms of position of the Cherenkov photons on the ground.
244
+ Get 1D distributions from numpy or boost histograms (1D and 2D).
245
+
246
+ Parameters
247
+ ----------
248
+ label: str
249
+ Histogram label.
250
+ hist: dict
251
+ Histogram dictionary.
845
252
 
846
253
  Returns
847
254
  -------
848
255
  numpy.ndarray
849
- The counts of the histogram.
256
+ Histogram counts.
850
257
  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")
258
+ Histogram x bin edges.
259
+ numpy.ndarray or None
260
+ Histogram uncertainties (if available).
261
+ """
262
+ # plain numpy histogram
263
+ if (
264
+ hist.get("projection") is None
265
+ and hasattr(self, "events")
266
+ and label in self.events.dtype.names
267
+ ):
268
+ return self._get_hist_1d_from_numpy(label, hist)
269
+
270
+ # boost 1D histogram
271
+ if hist.get("projection") is None:
272
+ # Use histogram from hist dict if available, otherwise from self.hist
273
+ if "histogram" in hist:
274
+ histo_1d = hist["histogram"]
275
+ else:
276
+ # No histogram available, return None values
277
+ return None, None, None
278
+ edges = histo_1d.axes.edges.T.flatten()[0]
279
+ view = histo_1d.view()
280
+ if self._check_for_all_attributes(view):
281
+ counts = np.asarray([view["value"].T])
282
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
283
+ else:
284
+ counts = np.asarray([view.T])
285
+ uncertainties = None
286
+ return counts, np.asarray([edges]), uncertainties
287
+
288
+ # boost 2D histogram projection
289
+ histo_2d = self.hist[hist["projection"][0]]["histogram"]
290
+ if hist["projection"][1] == "x":
291
+ h = histo_2d[:, sum]
292
+ else:
293
+ h = histo_2d[sum, :]
294
+ edges = h.axes.edges.T.flatten()[0]
295
+ view = h.view()
296
+ if self._check_for_all_attributes(view):
297
+ counts = np.asarray([view["value"].T])
298
+ uncertainties = np.asarray([np.sqrt(view["variance"].T)])
299
+ else:
300
+ counts = np.asarray([view.T])
301
+ uncertainties = None
302
+ return counts, np.asarray([edges]), uncertainties
856
303
 
857
- def get_2d_photon_density_distr(self):
304
+ def _set_1d_distributions(self, r_max=2000 * u.m, bins=100):
858
305
  """
859
- Get 2D histograms of position of the Cherenkov photons on the ground.
860
-
861
- It returns the photon density per square meter.
306
+ Define 1D histograms.
862
307
 
863
308
  Returns
864
309
  -------
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.
310
+ dict:
311
+ Dictionary with 1D histogram information.
871
312
  """
872
- return self._get_hist_2d_projection("density")
313
+ file_name = "file_name"
314
+ title = "title"
315
+ projection = "projection"
316
+ x_bins = "x_bins"
317
+ x_axis_unit = "x_axis_unit"
318
+ x_axis_title = "x_axis_title"
319
+ y_axis_unit = "y_axis_unit"
320
+ y_axis_title = "y_axis_title"
321
+ hist_1d = {
322
+ "wavelength": {
323
+ file_name: "hist_1d_photon_wavelength_distr",
324
+ title: "Photon wavelength distribution",
325
+ projection: ["wavelength_altitude", "x"],
326
+ },
327
+ "counts_r": {
328
+ file_name: "hist_1d_photon_radial_distr",
329
+ title: "Photon lateral distribution (ground level)",
330
+ x_bins: [bins, 0 * u.m, r_max, "linear"],
331
+ x_axis_title: "Distance to center",
332
+ x_axis_unit: u.m,
333
+ },
334
+ "density_r": {
335
+ file_name: "hist_1d_photon_density_distr",
336
+ title: "Photon lateral density distribution (ground level)",
337
+ x_bins: [bins, 0 * u.m, r_max, "linear"],
338
+ x_axis_title: "Distance to center",
339
+ x_axis_unit: u.m,
340
+ y_axis_title: "Photon density",
341
+ y_axis_unit: u.m**-2,
342
+ },
343
+ "density_x": {
344
+ file_name: "hist_1d_photon_density_x_distr",
345
+ title: "Photon lateral density x distribution (ground level)",
346
+ projection: ["density_xy", "x"],
347
+ },
348
+ "density_y": {
349
+ file_name: "hist_1d_photon_density_y_distr",
350
+ title: "Photon lateral density y distribution (ground level)",
351
+ projection: ["density_xy", "y"],
352
+ },
353
+ "time": {
354
+ file_name: "hist_1d_photon_time_distr",
355
+ title: "Photon arrival time distribution",
356
+ projection: ["time_altitude", "x"],
357
+ },
358
+ "altitude": {
359
+ file_name: "hist_1d_photon_altitude_distr",
360
+ title: "Photon emission altitude distribution",
361
+ projection: ["time_altitude", "y"],
362
+ },
363
+ "direction_cosine_x": {
364
+ file_name: "hist_1d_photon_direction_cosine_x_distr",
365
+ title: "Photon direction cosine x distribution",
366
+ projection: ["direction_xy", "x"],
367
+ },
368
+ "direction_cosine_y": {
369
+ file_name: "hist_1d_photon_direction_cosine_y_distr",
370
+ title: "Photon direction cosine y distribution",
371
+ projection: ["direction_xy", "y"],
372
+ },
373
+ "num_photons": {
374
+ file_name: "hist_1d_photon_per_event_distr",
375
+ title: "Photons per event distribution",
376
+ "event_type": True,
377
+ x_bins: [100, 0, None, "log"],
378
+ x_axis_title: "Cherenkov photons per event",
379
+ x_axis_unit: u.dimensionless_unscaled,
380
+ },
381
+ }
873
382
 
874
- def get_2d_photon_direction_distr(self):
383
+ for value in hist_1d.values():
384
+ value["is_1d"] = True
385
+ value["log_y"] = True
386
+ value[y_axis_title] = (
387
+ "Counts" if value.get(y_axis_title) is None else value[y_axis_title]
388
+ )
389
+ value[y_axis_unit] = (
390
+ u.dimensionless_unscaled if value.get(y_axis_unit) is None else value[y_axis_unit]
391
+ )
392
+ if value.get("projection") is not None:
393
+ hist_2d_name = value["projection"][0]
394
+ if value["projection"][1] == "x":
395
+ value[x_bins] = self.hist[hist_2d_name]["x_bins"]
396
+ value[x_axis_title] = self.hist[hist_2d_name]["x_axis_title"]
397
+ value[x_axis_unit] = self.hist[hist_2d_name]["x_axis_unit"]
398
+ else:
399
+ value[x_bins] = self.hist[hist_2d_name]["y_bins"]
400
+ value[x_axis_title] = self.hist[hist_2d_name]["y_axis_title"]
401
+ value[x_axis_unit] = self.hist[hist_2d_name]["y_axis_unit"]
402
+ elif value.get("event_type", False) is False:
403
+ boost_axes = self._create_regular_axes(value, ["x_bins"])
404
+ value["histogram"] = bh.Histogram(boost_axes[0], storage=bh.storage.Weight())
405
+ return hist_1d
406
+
407
+ def _set_2d_distributions(self, xy_maximum=1000 * u.m, xy_bin=100):
875
408
  """
876
- Get 2D histograms of incoming direction of the Cherenkov photons on the ground.
409
+ Define 2D histograms.
877
410
 
878
411
  Returns
879
412
  -------
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)
413
+ dict:
414
+ Dictionary with 2D histogram information.
886
415
  """
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
925
- )
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
-
930
- def _get_hist_1d_projection(self, label):
931
- """
932
- Get 1D distributions.
933
-
934
- Parameters
935
- ----------
936
- label: str
937
- Label to indicate which histogram.
938
-
939
- Returns
940
- -------
941
- numpy.ndarray
942
- The counts of the histogram.
943
- 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
1226
-
1227
- @property
1228
- def dict_1d_distributions(self):
1229
- """
1230
- Dictionary to label the 1D distributions according to the class methods.
1231
-
1232
- Returns
1233
- -------
1234
- dict:
1235
- The dictionary with information about the 1D distributions.
1236
- """
1237
- fn_key = "function"
1238
- file_name = "file name"
416
+ file_name = "file_name"
1239
417
  title = "title"
1240
- bin_edges = "bin edges"
1241
- axis_unit = "axis unit"
1242
- self._dict_1d_distributions = {
1243
- "wavelength": {
1244
- fn_key: "get_photon_wavelength_distr",
1245
- file_name: "hist_1d_photon_wavelength_distr",
1246
- title: "Photon wavelength distribution",
1247
- bin_edges: "wavelength",
1248
- axis_unit: self.hist_config["hist_position"][Z_AXIS_STRING]["start"].unit,
418
+ x_bins, y_bins = "x_bins", "y_bins"
419
+ x_axis_title, x_axis_unit = "x_axis_title", "x_axis_unit"
420
+ y_axis_title, y_axis_unit = "y_axis_title", "y_axis_unit"
421
+ z_axis_title, z_axis_unit = "z_axis_title", "z_axis_unit"
422
+
423
+ hist_2d = {
424
+ "counts_xy": {
425
+ file_name: "hist_2d_photon_count_distr",
426
+ title: "Photon count distribution (ground level)",
427
+ x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
428
+ y_bins: [xy_bin, -xy_maximum, xy_maximum],
429
+ x_axis_title: "x position on the ground",
430
+ x_axis_unit: xy_maximum.unit,
431
+ y_axis_title: "y position on the ground",
432
+ y_axis_unit: xy_maximum.unit,
1249
433
  },
1250
- "counts": {
1251
- fn_key: "get_photon_radial_distr",
1252
- 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,
434
+ "density_xy": {
435
+ file_name: "hist_2d_photon_density_distr",
436
+ title: "Photon lateral density distribution (ground level)",
437
+ x_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
438
+ y_bins: [xy_bin, -xy_maximum, xy_maximum, "linear"],
439
+ x_axis_title: "x position on the ground",
440
+ x_axis_unit: xy_maximum.unit,
441
+ y_axis_title: "y position on the ground",
442
+ y_axis_unit: xy_maximum.unit,
443
+ z_axis_title: "Photon density",
444
+ z_axis_unit: u.m**-2,
1256
445
  },
1257
- "density": {
1258
- fn_key: "get_photon_density_distr",
1259
- 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,
446
+ "direction_xy": {
447
+ file_name: "hist_2d_photon_direction_distr",
448
+ title: "Photon arrival direction",
449
+ x_bins: [100, -1, 1, "linear"],
450
+ y_bins: [100, -1, 1, "linear"],
451
+ x_axis_title: "x direction cosine",
452
+ x_axis_unit: u.dimensionless_unscaled,
453
+ y_axis_title: "y direction cosine",
454
+ y_axis_unit: u.dimensionless_unscaled,
1263
455
  },
1264
- "time": {
1265
- fn_key: "get_photon_time_of_emission_distr",
1266
- 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,
456
+ "time_altitude": {
457
+ file_name: "hist_2d_photon_time_altitude_distr",
458
+ title: "Arrival time vs emission altitude",
459
+ x_bins: [100, -2000 * u.ns, 2000 * u.ns, "linear"],
460
+ y_bins: [100, 120 * u.km, 0 * u.km, "linear"],
461
+ x_axis_title: "Arrival time",
462
+ x_axis_unit: u.ns,
463
+ y_axis_title: "Emission altitude",
464
+ y_axis_unit: u.km,
1270
465
  },
1271
- "altitude": {
1272
- fn_key: "get_photon_altitude_distr",
1273
- 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,
1277
- },
1278
- "num_photons_per_event": {
1279
- fn_key: "get_num_photons_per_event_distr",
1280
- file_name: "hist_1d_photon_per_event_distr",
1281
- 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,
466
+ "wavelength_altitude": {
467
+ file_name: "hist_2d_photon_wavelength_altitude_distr",
468
+ title: "Wavelength vs emission altitude ",
469
+ x_bins: [100, 100 * u.nm, 1000 * u.nm, "linear"],
470
+ y_bins: [100, 120 * u.km, 0 * u.km, "linear"],
471
+ x_axis_title: "Wavelength",
472
+ x_axis_unit: u.nm,
473
+ y_axis_title: "Emission altitude",
474
+ y_axis_unit: u.km,
1291
475
  },
1292
476
  }
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
477
 
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
1338
- else:
1339
- append = True
1340
- write_table(
1341
- table, self.hdf5_file_name, hdf5_table_name, append=append, overwrite=overwrite
1342
- )
1343
-
1344
- @property
1345
- def dict_2d_distributions(self):
1346
- """
1347
- Dictionary to label the 2D distributions according to the class methods.
1348
-
1349
- Returns
1350
- -------
1351
- dict:
1352
- The dictionary with information about the 2D distributions.
1353
- """
1354
- fn_key = "function"
1355
- file_name = "file name"
1356
- 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
1439
-
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],
478
+ for value in hist_2d.values():
479
+ value["is_1d"] = False
480
+ value["log_z"] = True
481
+ value[z_axis_title] = (
482
+ "Counts" if value.get(z_axis_title) is None else value[z_axis_title]
1444
483
  )
1445
-
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
- )
1452
- 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
1512
- 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.
1544
-
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
- )
1560
-
1561
- hdf5_table_name = f"/event_2d_histograms_{event_header_element_1}_{event_header_element_2}"
1562
-
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
1569
- else:
1570
- append = True
1571
- write_table(table, self.hdf5_file_name, hdf5_table_name, append=append, overwrite=overwrite)
1572
-
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.
1577
-
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
1585
- )
1586
- return self._num_photons_per_telescope
1587
-
1588
- @property
1589
- def total_num_photons(self):
1590
- """
1591
- The total number of photons.
1592
-
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.
1607
-
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,
484
+ value[z_axis_unit] = (
485
+ u.dimensionless_unscaled if value.get(z_axis_unit) is None else value[z_axis_unit]
1647
486
  )
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,
487
+ boost_axes = self._create_regular_axes(value, ["x_bins", "y_bins"])
488
+ value["histogram"] = bh.Histogram(
489
+ boost_axes[0], boost_axes[1], storage=bh.storage.Weight()
1681
490
  )
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.
1688
-
1689
- If negative, tracking starts at margin of atmosphere,
1690
- see TSTART in the CORSIKA 7 user guide.
1691
491
 
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,
1701
- )
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.
492
+ return hist_2d
1708
493
 
1709
- A tuple with Earth's magnetic field in the x and z directions are returned.
494
+ def _update_distributions(self):
495
+ """Update the distributions dictionary with the histogram values and bin edges."""
496
+ self._normalize_density_histograms()
1710
497
 
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,
1818
- )
1819
- return hist, bin_edges
498
+ for key, value in self.hist.items():
499
+ value["input_file_name"] = str(self.input_file)
500
+ if value["is_1d"]:
501
+ value["hist_values"], value["x_bin_edges"], value["uncertainties"] = (
502
+ self.get_hist_1d_projection(key, value)
503
+ )
504
+ else:
505
+ (
506
+ value["hist_values"],
507
+ value["x_bin_edges"],
508
+ value["y_bin_edges"],
509
+ value["uncertainties"],
510
+ ) = self.get_hist_2d_projection(value["histogram"])
511
+
512
+ def _normalize_density_histograms(self):
513
+ """Normalize the density histograms by the area of each bin."""
514
+
515
+ def normalize_histogram(hist, bin_areas):
516
+ view = hist.view()
517
+ if self._check_for_all_attributes(view):
518
+ view["value"] /= bin_areas
519
+ view["variance"] /= bin_areas**2
520
+ else:
521
+ hist /= bin_areas
1820
522
 
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.
523
+ def normalize_histogram_1d(hist, bin_areas):
524
+ view = hist.view()
525
+ if self._check_for_all_attributes(view):
526
+ view["value"] /= bin_areas
527
+ view["variance"] /= bin_areas**2
528
+ else:
529
+ view /= bin_areas
1824
530
 
1825
- Valid keys are stored in self.all_event_keys (CORSIKA defined).
531
+ density_xy_hist = self.hist["density_xy"]["histogram"]
532
+ density_r_hist = self.hist["density_r"]["histogram"]
1826
533
 
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.
1839
-
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.
534
+ bin_areas_xy = functools.reduce(operator.mul, density_xy_hist.axes.widths)
535
+ normalize_histogram(density_xy_hist, bin_areas_xy)
1848
536
 
537
+ bin_edges_r = density_r_hist.axes.edges[0]
538
+ bin_areas_r = np.pi * (bin_edges_r[1:] ** 2 - bin_edges_r[:-1] ** 2)
539
+ normalize_histogram_1d(density_r_hist, bin_areas_r)
1849
540
 
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
541
+ def _check_for_all_attributes(self, view):
542
+ """Check if view has dtype fields ('value', 'variance')."""
543
+ if hasattr(view, "dtype") and view.dtype.names == ("value", "variance"):
544
+ return True
545
+ return False