gammasimtools 0.19.0__py3-none-any.whl → 0.21.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 (59) hide show
  1. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -3
  2. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +54 -51
  3. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +3 -3
  4. simtools/_version.py +2 -2
  5. simtools/applications/calculate_incident_angles.py +182 -0
  6. simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
  7. simtools/applications/db_add_value_from_json_to_db.py +6 -9
  8. simtools/applications/db_generate_compound_indexes.py +7 -3
  9. simtools/applications/db_get_file_from_db.py +11 -23
  10. simtools/applications/derive_psf_parameters.py +58 -39
  11. simtools/applications/derive_trigger_rates.py +91 -0
  12. simtools/applications/generate_corsika_histograms.py +7 -184
  13. simtools/applications/maintain_simulation_model_add_production.py +105 -0
  14. simtools/applications/plot_simtel_events.py +5 -189
  15. simtools/applications/print_version.py +8 -7
  16. simtools/applications/validate_file_using_schema.py +7 -4
  17. simtools/configuration/commandline_parser.py +17 -11
  18. simtools/corsika/corsika_histograms.py +81 -0
  19. simtools/data_model/validate_data.py +8 -3
  20. simtools/db/db_handler.py +122 -31
  21. simtools/db/db_model_upload.py +51 -30
  22. simtools/dependencies.py +10 -5
  23. simtools/layout/array_layout_utils.py +37 -5
  24. simtools/model/array_model.py +18 -1
  25. simtools/model/model_repository.py +118 -63
  26. simtools/model/site_model.py +25 -0
  27. simtools/production_configuration/derive_corsika_limits.py +9 -34
  28. simtools/ray_tracing/incident_angles.py +706 -0
  29. simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
  30. simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
  31. simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
  32. simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
  33. simtools/schemas/model_parameters/stars.schema.yml +1 -1
  34. simtools/schemas/production_tables.schema.yml +5 -0
  35. simtools/simtel/simtel_config_writer.py +18 -20
  36. simtools/simtel/simtel_io_event_histograms.py +253 -516
  37. simtools/simtel/simtel_io_event_reader.py +51 -2
  38. simtools/simtel/simtel_io_event_writer.py +31 -11
  39. simtools/simtel/simtel_io_metadata.py +1 -1
  40. simtools/simtel/simtel_table_reader.py +3 -3
  41. simtools/simulator.py +1 -4
  42. simtools/telescope_trigger_rates.py +119 -0
  43. simtools/testing/log_inspector.py +13 -11
  44. simtools/utils/geometry.py +20 -0
  45. simtools/version.py +89 -0
  46. simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
  47. simtools/visualization/plot_incident_angles.py +431 -0
  48. simtools/visualization/plot_psf.py +673 -0
  49. simtools/visualization/plot_simtel_event_histograms.py +376 -0
  50. simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +284 -87
  51. simtools/visualization/visualize.py +1 -3
  52. simtools/applications/calculate_trigger_rate.py +0 -187
  53. simtools/applications/generate_sim_telarray_histograms.py +0 -196
  54. simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
  55. simtools/simtel/simtel_io_histogram.py +0 -623
  56. simtools/simtel/simtel_io_histograms.py +0 -556
  57. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
  58. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
  59. {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,10 @@
1
1
  """Histograms for shower and triggered events."""
2
2
 
3
+ import copy
3
4
  import logging
4
5
 
5
6
  import astropy.units as u
6
- import matplotlib.pyplot as plt
7
7
  import numpy as np
8
- from matplotlib.colors import LogNorm
9
8
 
10
9
  from simtools.simtel.simtel_io_event_reader import SimtelIOEventDataReader
11
10
 
@@ -15,6 +14,7 @@ class SimtelIOEventHistograms:
15
14
  Generate and fill histograms for shower and triggered events.
16
15
 
17
16
  Event data is read from the reduced MC event data file.
17
+ Calculate cumulative and relative (efficiency) distributions.
18
18
 
19
19
  Parameters
20
20
  ----------
@@ -31,54 +31,25 @@ class SimtelIOEventHistograms:
31
31
  self._logger = logging.getLogger(__name__)
32
32
  self.event_data_file = event_data_file
33
33
  self.array_name = array_name
34
- self.telescope_list = telescope_list
35
34
 
36
35
  self.histograms = {}
37
36
  self.file_info = {}
38
37
 
39
38
  self.reader = SimtelIOEventDataReader(event_data_file, telescope_list=telescope_list)
40
39
 
41
- def _fill_histogram_and_bin_edges(self, name, data, bins, hist1d=True):
42
- """
43
- Fill histogram and bin edges and it both to histogram dictionary.
44
-
45
- Adds histogram to existing histogram if it exists, otherwise initializes it.
46
-
47
- """
48
- if name in self.histograms:
49
- if hist1d:
50
- bins = self.histograms[f"{name}_bin_edges"]
51
- hist, _ = np.histogram(data, bins=bins)
52
- self.histograms[name] += hist
53
- else:
54
- x_bins = self.histograms[f"{name}_bin_x_edges"]
55
- y_bins = self.histograms[f"{name}_bin_y_edges"]
56
- hist, _, _ = np.histogram2d(data[0], data[1], bins=[x_bins, y_bins])
57
- self.histograms[name] += hist
58
- else:
59
- if hist1d:
60
- hist, bin_edges = np.histogram(data, bins=bins)
61
- self.histograms[name] = hist
62
- self.histograms[f"{name}_bin_edges"] = bin_edges
63
- else:
64
- hist, x_edges, y_edges = np.histogram2d(data[0], data[1], bins=bins)
65
- self.histograms[name] = hist
66
- self.histograms[f"{name}_bin_x_edges"] = x_edges
67
- self.histograms[f"{name}_bin_y_edges"] = y_edges
68
-
69
40
  def fill(self):
70
41
  """
71
42
  Fill histograms with event data.
72
43
 
73
44
  Involves looping over all event data, and therefore is the slowest part of the
74
- limit calculation. Adds the histograms to the histogram dictionary.
45
+ histogram module. Adds the histograms to the histogram dictionary.
75
46
 
76
47
  Assume that all event data files are generated with similar configurations
77
- (self.file_info contains the latest file info).
48
+ (self.file_info contains the file info of the last file).
78
49
  """
79
50
  for data_set in self.reader.data_sets:
80
51
  self._logger.info(f"Reading event data from {self.event_data_file} for {data_set}")
81
- _file_info_table, _, event_data, triggered_data = self.reader.read_event_data(
52
+ _file_info_table, shower_data, event_data, triggered_data = self.reader.read_event_data(
82
53
  self.event_data_file, table_name_map=data_set
83
54
  )
84
55
  _file_info_table = self.reader.get_reduced_simulation_file_info(_file_info_table)
@@ -86,533 +57,287 @@ class SimtelIOEventHistograms:
86
57
  "energy_min": _file_info_table["energy_min"].to("TeV"),
87
58
  "core_scatter_max": _file_info_table["core_scatter_max"].to("m"),
88
59
  "viewcone_max": _file_info_table["viewcone_max"].to("deg"),
60
+ "solid_angle": _file_info_table["solid_angle"].to("sr"),
61
+ "scatter_area": _file_info_table["scatter_area"].to("cm2"),
89
62
  }
90
63
 
91
- self._fill_histogram_and_bin_edges(
92
- "energy", event_data.simulated_energy, self.energy_bins
93
- )
94
- self._fill_histogram_and_bin_edges(
95
- "core_distance", event_data.core_distance_shower, self.core_distance_bins
96
- )
97
- self._fill_histogram_and_bin_edges(
98
- "angular_distance", triggered_data.angular_distance, self.view_cone_bins
99
- )
64
+ self.histograms = self._define_histograms(event_data, triggered_data, shower_data)
100
65
 
101
- xy_bins = np.linspace(
102
- -1.0 * self.core_distance_bins.max(),
103
- self.core_distance_bins.max(),
104
- len(self.core_distance_bins),
105
- )
106
- self._fill_histogram_and_bin_edges(
107
- "shower_cores",
108
- (event_data.x_core_shower, event_data.y_core_shower),
109
- [xy_bins, xy_bins],
110
- hist1d=False,
111
- )
112
- self._fill_histogram_and_bin_edges(
113
- "core_vs_energy",
114
- (event_data.core_distance_shower, event_data.simulated_energy),
115
- [self.core_distance_bins, self.energy_bins],
116
- hist1d=False,
117
- )
118
- self._fill_histogram_and_bin_edges(
119
- "angular_distance_vs_energy",
120
- (triggered_data.angular_distance, event_data.simulated_energy),
121
- [self.view_cone_bins, self.energy_bins],
122
- hist1d=False,
123
- )
66
+ for name, data in self.histograms.items():
67
+ self._logger.debug(f"Filling histogram {name}")
68
+ self._fill_histogram_and_bin_edges(data)
124
69
 
125
- @property
126
- def energy_bins(self):
127
- """Return bins for the energy histogram."""
128
- if "energy_bin_edges" in self.histograms:
129
- return self.histograms["energy_bin_edges"]
130
- return np.logspace(
131
- np.log10(self.file_info.get("energy_min", 1.0e-3 * u.TeV).to("TeV").value),
132
- np.log10(self.file_info.get("energy_max", 1.0e3 * u.TeV).to("TeV").value),
133
- 100,
134
- )
70
+ self.print_summary()
135
71
 
136
- @property
137
- def core_distance_bins(self):
138
- """Return bins for the core distance histogram."""
139
- if "core_distance_bin_edges" in self.histograms:
140
- return self.histograms["core_distance_bin_edges"]
141
- return np.linspace(
142
- self.file_info.get("core_scatter_min", 0.0 * u.m).to("m").value,
143
- self.file_info.get("core_scatter_max", 1.0e5 * u.m).to("m").value,
144
- 100,
145
- )
72
+ self.calculate_efficiency_data()
73
+ self.calculate_cumulative_data()
146
74
 
147
- @property
148
- def view_cone_bins(self):
149
- """Return bins for the viewcone histogram."""
150
- if "viewcone_bin_edges" in self.histograms:
151
- return self.histograms["viewcone_bin_edges"]
152
- return np.linspace(
153
- self.file_info.get("viewcone_min", 0.0 * u.deg).to("deg").value,
154
- self.file_info.get("viewcone_max", 20.0 * u.deg).to("deg").value,
155
- 100,
156
- )
157
-
158
- def plot_data(self, output_path=None, limits=None, rebin_factor=2):
75
+ def _define_histograms(self, event_data, triggered_data, shower_data):
159
76
  """
160
- Histogram plotting.
77
+ Define histograms including event data, binning, naming, and labels.
78
+
79
+ All histograms are defined for simulated and triggered events (note
80
+ the subtlety of triggered events being read from event_data and triggered_data).
161
81
 
162
82
  Parameters
163
83
  ----------
164
- output_path: Path or str, optional
165
- Directory to save plots. If None, plots will be displayed.
166
- limits: dict, optional
167
- Dictionary containing limits for plotting. Keys can include:
168
- - "upper_radius_limit": Upper limit for core distance
169
- - "lower_energy_limit": Lower limit for energy
170
- - "viewcone_radius": Radius for the viewcone
171
- rebin_factor: int, optional
172
- Factor by which to reduce the number of bins in 2D histograms for rebinned plots.
173
- Default is 2 (merge every 2 bins). Set to 0 or 1 to disable rebinning.
174
- """
175
- # Plot label constants
176
- core_distance_label = "Core Distance [m]"
177
- energy_label = "Energy [TeV]"
178
- pointing_direction_label = "Distance to pointing direction [deg]"
179
- cumulative_prefix = "Cumulative "
180
- event_count_label = "Event Count"
181
- core_x_label = "Core X [m]"
182
- core_y_label = "Core Y [m]"
183
-
184
- # Plot parameter constants
185
- hist_1d_params = {"color": "tab:green", "edgecolor": "tab:green", "lw": 1}
186
- hist_1d_cumulative_params = {"color": "tab:blue", "edgecolor": "tab:blue", "lw": 1}
187
- hist_2d_params = {"norm": "log", "cmap": "viridis", "show_contour": False}
188
- hist_2d_equal_params = {
189
- "norm": "log",
190
- "cmap": "viridis",
191
- "aspect": "equal",
192
- "show_contour": False,
193
- }
194
- hist_2d_normalized_params = {"norm": "linear", "cmap": "viridis", "show_contour": True}
195
-
196
- self._logger.info(f"Plotting histograms written to {output_path}")
84
+ event_data : EventData
85
+ The event data to use for filling the histograms.
86
+ triggered_data : TriggeredData
87
+ The triggered data to use for filling the histograms.
88
+ shower_data : ShowerData
89
+ The shower data to use for filling the histograms.
197
90
 
198
- angular_dist_vs_energy = self.histograms.get("angular_distance_vs_energy")
199
- normalized_cumulative_angular_vs_energy = self._calculate_cumulative_histogram(
200
- angular_dist_vs_energy, axis=0, normalize=True
201
- )
202
-
203
- core_vs_energy = self.histograms.get("core_vs_energy")
204
- normalized_cumulative_core_vs_energy = self._calculate_cumulative_histogram(
205
- core_vs_energy, axis=0, normalize=True
91
+ Returns
92
+ -------
93
+ dict
94
+ Dictionary with histogram definitions.
95
+ """
96
+ xy_bins = np.linspace(
97
+ -1.0 * self.core_distance_bins.max(),
98
+ self.core_distance_bins.max(),
99
+ len(self.core_distance_bins),
206
100
  )
207
-
208
- energy_hist = self.histograms.get("energy")
209
- cumulative_energy = self._calculate_cumulative_histogram(energy_hist, reverse=True)
210
-
211
- core_distance_hist = self.histograms.get("core_distance")
212
- cumulative_core_distance = self._calculate_cumulative_histogram(core_distance_hist)
213
-
214
- angular_distance_hist = self.histograms.get("angular_distance")
215
- cumulative_angular_distance = self._calculate_cumulative_histogram(angular_distance_hist)
216
-
217
- upper_radius_limit, lower_energy_limit, viewcone_radius = self._get_limits(limits)
218
-
219
- plots = {
220
- "core_vs_energy": {
221
- "data": self.histograms.get("core_vs_energy"),
222
- "bins": [
223
- self.histograms.get("core_vs_energy_bin_x_edges"),
224
- self.histograms.get("core_vs_energy_bin_y_edges"),
225
- ],
226
- "plot_type": "histogram2d",
227
- "plot_params": hist_2d_params,
228
- "labels": {
229
- "x": core_distance_label,
230
- "y": energy_label,
231
- "title": "Triggered events: core distance vs energy",
232
- },
233
- "lines": {"x": upper_radius_limit, "y": lower_energy_limit},
234
- "scales": {"y": "log"},
235
- "colorbar_label": event_count_label,
236
- "filename": "core_vs_energy_distribution",
237
- },
238
- "energy_distribution": {
239
- "data": self.histograms.get("energy"),
240
- "bins": self.histograms.get("energy_bin_edges"),
241
- "plot_type": "histogram",
242
- "plot_params": hist_1d_params,
243
- "labels": {
244
- "x": energy_label,
245
- "y": event_count_label,
246
- "title": "Triggered events: energy distribution",
247
- },
248
- "scales": {"x": "log", "y": "log"},
249
- "lines": {"x": lower_energy_limit},
250
- "filename": "energy_distribution",
251
- },
252
- "energy_distribution_cumulative": {
253
- "data": cumulative_energy,
254
- "bins": self.histograms.get("energy_bin_edges"),
255
- "plot_type": "histogram",
256
- "plot_params": hist_1d_cumulative_params,
257
- "labels": {
258
- "x": energy_label,
259
- "y": cumulative_prefix + event_count_label,
260
- "title": "Triggered events: cumulative energy distribution",
261
- },
262
- "scales": {"x": "log", "y": "log"},
263
- "lines": {"x": lower_energy_limit},
264
- "filename": "energy_distribution_cumulative",
101
+ hists = {}
102
+
103
+ energy_axis_title = "Energy (TeV)"
104
+ event_count_axis_title = "Event Count"
105
+
106
+ definitions = {
107
+ "energy": {
108
+ "event_data_column": "simulated_energy",
109
+ "event_data": event_data,
110
+ "bin_edges": self.energy_bins,
111
+ "axis_titles": [energy_axis_title, event_count_axis_title],
112
+ "plot_scales": {"x": "log", "y": "log"},
265
113
  },
266
114
  "core_distance": {
267
- "data": self.histograms.get("core_distance"),
268
- "bins": self.histograms.get("core_distance_bin_edges"),
269
- "plot_type": "histogram",
270
- "plot_params": hist_1d_params,
271
- "labels": {
272
- "x": core_distance_label,
273
- "y": event_count_label,
274
- "title": "Triggered events: core distance distribution",
275
- },
276
- "lines": {"x": upper_radius_limit},
277
- "filename": "core_distance_distribution",
278
- },
279
- "core_distance_cumulative": {
280
- "data": cumulative_core_distance,
281
- "bins": self.histograms.get("core_distance_bin_edges"),
282
- "plot_type": "histogram",
283
- "plot_params": hist_1d_cumulative_params,
284
- "labels": {
285
- "x": core_distance_label,
286
- "y": cumulative_prefix + event_count_label,
287
- "title": "Triggered events: cumulative core distance distribution",
288
- },
289
- "lines": {"x": upper_radius_limit},
290
- "filename": "core_distance_cumulative_distribution",
291
- },
292
- "core_xy": {
293
- "data": self.histograms.get("shower_cores"),
294
- "bins": [
295
- self.histograms.get("shower_cores_bin_x_edges"),
296
- self.histograms.get("shower_cores_bin_y_edges"),
297
- ],
298
- "plot_type": "histogram2d",
299
- "plot_params": hist_2d_equal_params,
300
- "labels": {
301
- "x": core_x_label,
302
- "y": core_y_label,
303
- "title": "Triggered events: core x vs core y",
304
- },
305
- "colorbar_label": event_count_label,
306
- "lines": {
307
- "r": upper_radius_limit,
308
- },
309
- "filename": "core_xy_distribution",
115
+ "event_data_column": "core_distance_shower",
116
+ "event_data": event_data,
117
+ "bin_edges": self.core_distance_bins,
118
+ "axis_titles": ["Core Distance (m)", event_count_axis_title],
310
119
  },
311
120
  "angular_distance": {
312
- "data": self.histograms.get("angular_distance"),
313
- "bins": self.histograms.get("angular_distance_bin_edges"),
314
- "plot_type": "histogram",
315
- "plot_params": hist_1d_params,
316
- "labels": {
317
- "x": pointing_direction_label,
318
- "y": event_count_label,
319
- "title": "Triggered events: angular distance distribution",
320
- },
321
- "lines": {"x": viewcone_radius},
322
- "filename": "angular_distance_distribution",
323
- },
324
- "angular_distance_cumulative": {
325
- "data": cumulative_angular_distance,
326
- "bins": self.histograms.get("angular_distance_bin_edges"),
327
- "plot_type": "histogram",
328
- "plot_params": hist_1d_cumulative_params,
329
- "labels": {
330
- "x": pointing_direction_label,
331
- "y": cumulative_prefix + event_count_label,
332
- "title": "Triggered events: cumulative angular distance distribution",
333
- },
334
- "lines": {"x": viewcone_radius},
335
- "filename": "angular_distance_cumulative_distribution",
121
+ "event_data_column": "angular_distance",
122
+ "event_data": triggered_data,
123
+ "bin_edges": self.view_cone_bins,
124
+ "axis_titles": ["Angular Distance (deg)", event_count_axis_title],
336
125
  },
337
- "angular_distance_vs_energy": {
338
- "data": self.histograms.get("angular_distance_vs_energy"),
339
- "bins": [
340
- self.histograms.get("angular_distance_vs_energy_bin_x_edges"),
341
- self.histograms.get("angular_distance_vs_energy_bin_y_edges"),
342
- ],
343
- "plot_type": "histogram2d",
344
- "plot_params": hist_2d_params,
345
- "labels": {
346
- "x": pointing_direction_label,
347
- "y": energy_label,
348
- "title": "Triggered events: angular distance distance vs energy",
349
- },
350
- "lines": {
351
- "x": viewcone_radius,
352
- "y": lower_energy_limit,
353
- },
354
- "scales": {"y": "log"},
355
- "colorbar_label": event_count_label,
356
- "filename": "angular_distance_vs_energy_distribution",
126
+ "x_core_shower_vs_y_core_shower": {
127
+ "event_data_column": ("x_core_shower", "y_core_shower"),
128
+ "event_data": (event_data, event_data),
129
+ "bin_edges": (xy_bins, xy_bins),
130
+ "is_1d": False,
131
+ "axis_titles": ["Core X (m)", "Core Y (m)", event_count_axis_title],
357
132
  },
358
- "angular_distance_vs_energy_cumulative": {
359
- "data": normalized_cumulative_angular_vs_energy,
360
- "bins": [
361
- self.histograms.get("angular_distance_vs_energy_bin_x_edges"),
362
- self.histograms.get("angular_distance_vs_energy_bin_y_edges"),
363
- ],
364
- "plot_type": "histogram2d",
365
- "plot_params": hist_2d_normalized_params, # Includes contour line at value=1
366
- "labels": {
367
- "x": pointing_direction_label,
368
- "y": energy_label,
369
- "title": "Triggered events: fraction of events by angular distance vs energy",
370
- },
371
- "lines": {
372
- "x": viewcone_radius,
373
- "y": lower_energy_limit,
374
- },
375
- "scales": {"y": "log"},
376
- "colorbar_label": "Fraction of events",
377
- "filename": "angular_distance_vs_energy_cumulative_distribution",
133
+ "core_vs_energy": {
134
+ "event_data_column": ("core_distance_shower", "simulated_energy"),
135
+ "event_data": (event_data, event_data),
136
+ "bin_edges": (self.core_distance_bins, self.energy_bins),
137
+ "is_1d": False,
138
+ "axis_titles": ["Core Distance (m)", energy_axis_title, event_count_axis_title],
139
+ "plot_scales": {"y": "log"},
378
140
  },
379
- "core_vs_energy_cumulative": {
380
- "data": normalized_cumulative_core_vs_energy,
381
- "bins": [
382
- self.histograms.get("core_vs_energy_bin_x_edges"),
383
- self.histograms.get("core_vs_energy_bin_y_edges"),
141
+ "angular_distance_vs_energy": {
142
+ "event_data_column": ("angular_distance", "simulated_energy"),
143
+ "event_data": (triggered_data, event_data),
144
+ "bin_edges": (self.view_cone_bins, self.energy_bins),
145
+ "is_1d": False,
146
+ "axis_titles": [
147
+ "Angular Distance (deg)",
148
+ energy_axis_title,
149
+ event_count_axis_title,
384
150
  ],
385
- "plot_type": "histogram2d",
386
- "plot_params": hist_2d_normalized_params,
387
- "labels": {
388
- "x": core_distance_label,
389
- "y": energy_label,
390
- "title": "Triggered events: fraction of events by core distance vs energy",
391
- },
392
- "lines": {
393
- "x": upper_radius_limit,
394
- "y": lower_energy_limit,
395
- },
396
- "scales": {"y": "log"},
397
- "colorbar_label": "Fraction of events",
398
- "filename": "core_vs_energy_cumulative_distribution",
151
+ "plot_scales": {"y": "log"},
399
152
  },
400
153
  }
401
154
 
402
- for plot_key, plot_args in plots.items():
403
- plot_filename = plot_args.pop("filename")
404
- if self.array_name and plot_args.get("labels", {}).get("title"):
405
- plot_args["labels"]["title"] += f" ({self.array_name} array)"
406
-
407
- filename = self._build_plot_filename(plot_filename, self.array_name)
408
- output_file = output_path / filename if output_path else None
409
- self._create_plot(**plot_args, output_file=output_file)
410
-
411
- if self._should_create_rebinned_plot(rebin_factor, plot_args, plot_key):
412
- self._create_rebinned_plot(plot_args, filename, output_path, rebin_factor)
413
-
414
- def _get_limits(self, limits):
415
- """Extract limits from the provided dictionary for plotting."""
416
- upper_radius_limit = None
417
- lower_energy_limit = None
418
- viewcone_radius = None
419
- if limits:
420
- upper_radius_limit = (
421
- limits["upper_radius_limit"].value if "upper_radius_limit" in limits else None
422
- )
423
- lower_energy_limit = (
424
- limits["lower_energy_limit"].value if "lower_energy_limit" in limits else None
425
- )
426
- viewcone_radius = (
427
- limits["viewcone_radius"].value if "viewcone_radius" in limits else None
155
+ hists = {
156
+ name: self.get_histogram_definition(**cfg) | {"suffix": "", "title": "Triggered Events"}
157
+ for name, cfg in definitions.items()
158
+ }
159
+
160
+ hists_mc = {}
161
+ for key, hist in hists.items():
162
+ key_mc = f"{key}_mc"
163
+ hists_mc[key_mc] = copy.copy(hist)
164
+ hists_mc[key_mc]["suffix"] = "_mc"
165
+ hists_mc[key_mc]["title"] = "Simulated Events"
166
+ hists_mc[key_mc]["event_data"] = (
167
+ shower_data if hist["1d"] else (shower_data, shower_data)
428
168
  )
429
- return upper_radius_limit, lower_energy_limit, viewcone_radius
430
169
 
431
- def _build_plot_filename(self, base_filename, array_name=None):
432
- """
433
- Build the full plot filename with appropriate extensions.
170
+ hists.update(hists_mc)
171
+ return hists
434
172
 
435
- Parameters
436
- ----------
437
- base_filename : str
438
- The base filename without extension
439
- array_name : str, optional
440
- Name of the array to append to filename
173
+ def get_histogram_definition(
174
+ self,
175
+ event_data_column=None,
176
+ event_data=None,
177
+ histogram=None,
178
+ bin_edges=None,
179
+ title=None,
180
+ axis_titles=None,
181
+ suffix=None,
182
+ is_1d=True,
183
+ plot_scales=None,
184
+ ):
185
+ """Return a single histogram definition."""
186
+ return {
187
+ "histogram": histogram,
188
+ "event_data_column": event_data_column,
189
+ "event_data": event_data,
190
+ "1d": is_1d,
191
+ "bin_edges": bin_edges,
192
+ "title": title,
193
+ "axis_titles": axis_titles,
194
+ "suffix": suffix,
195
+ "plot_scales": plot_scales,
196
+ }
441
197
 
442
- Returns
443
- -------
444
- str
445
- Complete filename with extension
198
+ def _fill_histogram_and_bin_edges(self, data):
446
199
  """
447
- if array_name:
448
- return f"{base_filename}_{array_name}.png"
449
- return f"{base_filename}.png"
200
+ Fill histogram and bin edges into the histogram dictionary.
450
201
 
451
- def _should_create_rebinned_plot(self, rebin_factor, plot_args, plot_key):
202
+ Adds to existing histogram if present, otherwise initializes it.
452
203
  """
453
- Check if a rebinned version of the plot should be created.
204
+ if data["1d"]:
205
+ hist, _ = np.histogram(
206
+ getattr(data["event_data"], data["event_data_column"]),
207
+ bins=data["bin_edges"],
208
+ )
209
+ else:
210
+ hist, _, _ = np.histogram2d(
211
+ getattr(data["event_data"][0], data["event_data_column"][0]),
212
+ getattr(data["event_data"][1], data["event_data_column"][1]),
213
+ bins=[data["bin_edges"][0], data["bin_edges"][1]],
214
+ )
454
215
 
455
- Parameters
456
- ----------
457
- rebin_factor : int
458
- Factor by which to rebin the energy axis
459
- plot_args : dict
460
- Plot arguments
461
- plot_key : str
462
- Key identifying the plot type
216
+ data["histogram"] = hist if data["histogram"] is None else data["histogram"] + hist
463
217
 
464
- Returns
465
- -------
466
- bool
467
- True if a rebinned plot should be created, False otherwise
218
+ def calculate_efficiency_data(self):
468
219
  """
469
- return (
470
- rebin_factor > 1
471
- and plot_args["plot_type"] == "histogram2d"
472
- and plot_key.endswith("_cumulative")
473
- and plot_args.get("plot_params", {}).get("norm") == "linear"
474
- )
220
+ Calculate efficiency histograms (triggered divided by simulated).
475
221
 
476
- def _create_rebinned_plot(self, plot_args, filename, output_path, rebin_factor):
477
- """
478
- Create a rebinned version of a 2D histogram plot.
222
+ Assumes that for each histogram with simulated events, there is a
223
+ corresponding histogram with triggered events.
479
224
 
480
- Parameters
481
- ----------
482
- plot_args : dict
483
- Plot arguments for the original plot
484
- filename : str
485
- Filename of the original plot
486
- output_path : Path or None
487
- Path to save the plot to, or None
488
- rebin_factor : int
489
- Factor by which to rebin the energy axis
225
+ Returns
226
+ -------
227
+ dict
228
+ Dictionary containing the efficiency histograms.
490
229
  """
491
- data = plot_args["data"]
492
- bins = plot_args["bins"]
493
230
 
494
- rebinned_data, rebinned_x_bins, rebinned_y_bins = self._rebin_2d_histogram(
495
- data, bins[0], bins[1], rebin_factor
496
- )
231
+ def calculate_efficiency(trig_hist, mc_hist):
232
+ with np.errstate(divide="ignore", invalid="ignore"):
233
+ return np.divide(
234
+ trig_hist,
235
+ mc_hist,
236
+ out=np.zeros_like(trig_hist, dtype=float),
237
+ where=mc_hist > 0,
238
+ )
497
239
 
498
- rebinned_plot_args = plot_args.copy()
499
- rebinned_plot_args["data"] = rebinned_data
500
- rebinned_plot_args["bins"] = [rebinned_x_bins, rebinned_y_bins]
240
+ eff_histograms = {}
241
+ for name, mc_hist in self.histograms.items():
242
+ if not name.endswith("_mc"):
243
+ continue
501
244
 
502
- if rebinned_plot_args.get("labels", {}).get("title"):
503
- rebinned_plot_args["labels"]["title"] += f" (Energy rebinned {rebin_factor}x)"
245
+ base_name = name[:-3]
246
+ trig_hist = self.histograms.get(base_name)
247
+ if trig_hist is None:
248
+ continue
504
249
 
505
- rebinned_filename = f"{filename.replace('.png', '')}_rebinned.png"
506
- rebinned_output_file = output_path / rebinned_filename if output_path else None
507
- self._create_plot(**rebinned_plot_args, output_file=rebinned_output_file)
250
+ if mc_hist["histogram"].shape != trig_hist["histogram"].shape:
251
+ self._logger.warning(
252
+ f"Shape mismatch for {base_name} and {name}, skipping efficiency calculation."
253
+ )
254
+ continue
255
+
256
+ eff = copy.copy(mc_hist)
257
+ eff.update(
258
+ {
259
+ "histogram": calculate_efficiency(trig_hist["histogram"], mc_hist["histogram"]),
260
+ "suffix": "_eff",
261
+ "title": "Efficiency",
262
+ }
263
+ )
264
+ eff["axis_titles"] = copy.copy(mc_hist["axis_titles"])
265
+ eff["axis_titles"][-1] = "Efficiency"
266
+ eff_histograms[f"{base_name}_eff"] = eff
508
267
 
509
- def _create_plot(
510
- self,
511
- data,
512
- bins=None,
513
- plot_type="histogram",
514
- plot_params=None,
515
- labels=None,
516
- scales=None,
517
- colorbar_label=None,
518
- output_file=None,
519
- lines=None,
520
- ):
521
- """
522
- Create and save a plot with the given parameters.
268
+ self.histograms.update(eff_histograms)
269
+ return eff_histograms
523
270
 
524
- For normalized 2D histograms, a contour line is drawn at the value of 1.0
525
- to indicate the boundary where each energy bin reaches complete containment.
526
- This can be controlled with the 'show_contour' parameter in plot_params.
527
- """
528
- plot_params = plot_params or {}
529
- labels = labels or {}
530
- scales = scales or {}
531
- lines = lines or {}
532
-
533
- fig, ax = plt.subplots(figsize=(8, 6))
534
-
535
- if plot_type == "histogram":
536
- plt.bar(bins[:-1], data, width=np.diff(bins), **plot_params)
537
- elif plot_type == "histogram2d":
538
- pcm = self._create_2d_histogram_plot(data, bins, plot_params)
539
- plt.colorbar(pcm, label=colorbar_label)
540
-
541
- if "x" in lines:
542
- plt.axvline(lines["x"], color="r", linestyle="--", linewidth=0.5)
543
- if "y" in lines:
544
- plt.axhline(lines["y"], color="r", linestyle="--", linewidth=0.5)
545
- if "r" in lines:
546
- circle = plt.Circle(
547
- (0, 0), lines["r"], color="r", fill=False, linestyle="--", linewidth=0.5
548
- )
549
- plt.gca().add_artist(circle)
550
-
551
- ax.set(
552
- xlabel=labels.get("x", ""),
553
- ylabel=labels.get("y", ""),
554
- title=labels.get("title", ""),
555
- xscale=scales.get("x", "linear"),
556
- yscale=scales.get("y", "linear"),
271
+ @property
272
+ def energy_bins(self):
273
+ """Return bins for the energy histogram."""
274
+ if "energy_bin_edges" in self.histograms:
275
+ return self.histograms["energy_bin_edges"]
276
+ return np.logspace(
277
+ np.log10(self.file_info.get("energy_min", 1.0e-3 * u.TeV).to("TeV").value),
278
+ np.log10(self.file_info.get("energy_max", 1.0e3 * u.TeV).to("TeV").value),
279
+ 100,
557
280
  )
558
281
 
559
- if output_file:
560
- self._logger.info(f"Saving plot to {output_file}")
561
- plt.savefig(output_file, dpi=300, bbox_inches="tight")
562
- plt.close()
563
- else:
564
- plt.tight_layout()
565
- plt.show()
282
+ @property
283
+ def core_distance_bins(self):
284
+ """Return bins for the core distance histogram."""
285
+ if "core_distance_bin_edges" in self.histograms:
286
+ return self.histograms["core_distance_bin_edges"]
287
+ return np.linspace(
288
+ self.file_info.get("core_scatter_min", 0.0 * u.m).to("m").value,
289
+ self.file_info.get("core_scatter_max", 1.0e5 * u.m).to("m").value,
290
+ 100,
291
+ )
566
292
 
567
- return fig
293
+ @property
294
+ def view_cone_bins(self):
295
+ """Return bins for the viewcone histogram."""
296
+ if "viewcone_bin_edges" in self.histograms:
297
+ return self.histograms["viewcone_bin_edges"]
298
+ return np.linspace(
299
+ self.file_info.get("viewcone_min", 0.0 * u.deg).to("deg").value,
300
+ self.file_info.get("viewcone_max", 20.0 * u.deg).to("deg").value,
301
+ 100,
302
+ )
568
303
 
569
- def _create_2d_histogram_plot(self, data, bins, plot_params):
304
+ def calculate_cumulative_data(self):
570
305
  """
571
- Create a 2D histogram plot with the given parameters.
572
-
573
- Parameters
574
- ----------
575
- data : np.ndarray
576
- 2D histogram data
577
- bins : tuple of np.ndarray
578
- Bin edges for x and y axes
579
- plot_params : dict
580
- Plot parameters including norm, cmap, and show_contour
306
+ Calculate cumulative distributions for triggered histograms.
581
307
 
582
308
  Returns
583
309
  -------
584
- matplotlib.collections.QuadMesh
585
- The created pcolormesh object for colorbar attachment
310
+ dict
311
+ Dictionary containing the cumulative histograms.
586
312
  """
587
- if plot_params.get("norm") == "linear":
588
- pcm = plt.pcolormesh(
589
- bins[0],
590
- bins[1],
591
- data.T,
592
- vmin=0,
593
- vmax=1,
594
- cmap=plot_params.get("cmap", "viridis"),
595
- )
596
- # Add contour line at value=1.0 for normalized histograms
597
- if plot_params.get("show_contour", True):
598
- x_centers = (bins[0][1:] + bins[0][:-1]) / 2
599
- y_centers = (bins[1][1:] + bins[1][:-1]) / 2
600
- x_mesh, y_mesh = np.meshgrid(x_centers, y_centers)
601
- plt.contour(
602
- x_mesh,
603
- y_mesh,
604
- data.T,
605
- levels=[0.999999], # very close to 1 for floating point precision
606
- colors=["tab:red"],
607
- linestyles=["--"],
608
- linewidths=[0.5],
609
- )
610
- else:
611
- pcm = plt.pcolormesh(
612
- bins[0], bins[1], data.T, norm=LogNorm(vmin=1, vmax=data.max()), cmap="viridis"
313
+ cumulative_data = {}
314
+ suffix = "_cumulative"
315
+
316
+ def add_cumulative(name, hist, **kwargs):
317
+ new = copy.copy(hist)
318
+ new["histogram"] = self._calculate_cumulative_histogram(hist["histogram"], **kwargs)
319
+ new["axis_titles"] = copy.copy(hist["axis_titles"])
320
+ new.update(
321
+ {
322
+ "suffix": suffix,
323
+ "title": "Cumulative triggered events",
324
+ }
613
325
  )
326
+ new["axis_titles"][-1] = "Fraction of Events"
327
+ cumulative_data[f"{name}{suffix}"] = new
614
328
 
615
- return pcm
329
+ # 2D histograms vs energy
330
+ for name, hist in self.histograms.items():
331
+ if name.endswith("_vs_energy") and not name.endswith("_mc"):
332
+ add_cumulative(name, hist, axis=0, normalize=True)
333
+
334
+ # 1D histograms
335
+ for name in ["energy", "core_distance", "angular_distance"]:
336
+ if (hist := self.histograms.get(name)) is not None:
337
+ add_cumulative(name, hist, reverse=name == "energy")
338
+
339
+ self.histograms.update(cumulative_data)
340
+ return cumulative_data
616
341
 
617
342
  def _calculate_cumulative_histogram(self, hist, reverse=False, axis=None, normalize=False):
618
343
  """
@@ -647,12 +372,13 @@ class SimtelIOEventHistograms:
647
372
  result = result / np.sum(hist)
648
373
  return result
649
374
 
650
- if axis is None:
651
- axis = 1
652
-
375
+ axis = axis if axis is not None else 1
653
376
  result = self._apply_cumsum_along_axis(hist.copy(), axis, reverse)
654
377
 
655
378
  if normalize:
379
+ # Ensure floating dtype to allow in-place normalization without casting errors
380
+ if not np.issubdtype(result.dtype, np.floating):
381
+ result = result.astype(float)
656
382
  self._normalize_along_axis(result, hist, axis)
657
383
 
658
384
  return result
@@ -693,9 +419,7 @@ class SimtelIOEventHistograms:
693
419
 
694
420
  def _calculate_cumulative_2d(self, hist, reverse, axis=None):
695
421
  """Calculate cumulative distribution for 2D histogram."""
696
- if axis is None:
697
- axis = 1
698
-
422
+ axis = axis if axis is not None else 1
699
423
  return self._apply_cumsum_along_axis(hist, axis, reverse)
700
424
 
701
425
  def _apply_cumsum_along_axis(self, hist, axis, reverse):
@@ -706,7 +430,8 @@ class SimtelIOEventHistograms:
706
430
 
707
431
  return np.apply_along_axis(cumsum_func, axis, hist)
708
432
 
709
- def _rebin_2d_histogram(self, hist, x_bins, y_bins, rebin_factor=2):
433
+ @staticmethod
434
+ def rebin_2d_histogram(hist, x_bins, y_bins, rebin_factor=2):
710
435
  """
711
436
  Rebin a 2D histogram by merging neighboring bins along the energy dimension (y-axis) only.
712
437
 
@@ -725,7 +450,7 @@ class SimtelIOEventHistograms:
725
450
  Returns
726
451
  -------
727
452
  tuple
728
- (rebinned_hist, x_bins, rebinned_y_bins)
453
+ (re-binned_hist, x_bins, re-binned_y_bins)
729
454
  """
730
455
  if rebin_factor <= 1:
731
456
  return hist, x_bins, y_bins
@@ -744,3 +469,15 @@ class SimtelIOEventHistograms:
744
469
  new_y_bins = y_bins[::rebin_factor]
745
470
 
746
471
  return new_hist, x_bins, new_y_bins
472
+
473
+ def print_summary(self):
474
+ """
475
+ Print a summary of the histogram statistics.
476
+
477
+ Total number of events is retrieved from the 'energy' histograms.
478
+ """
479
+ total_simulated = np.sum(self.histograms.get("energy_mc", {}).get("histogram", []))
480
+ total_triggered = np.sum(self.histograms.get("energy", {}).get("histogram", []))
481
+
482
+ self._logger.info(f"Total simulated events: {total_simulated}")
483
+ self._logger.info(f"Total triggered events: {total_triggered}")