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
@@ -0,0 +1,376 @@
1
+ """Plot simtel event histograms filled with SimtelIOEventHistograms."""
2
+
3
+ import logging
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+ from matplotlib.colors import LogNorm
8
+
9
+ from simtools.simtel.simtel_io_event_histograms import SimtelIOEventHistograms
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ def plot(histograms, output_path=None, limits=None, rebin_factor=2, array_name=None):
15
+ """
16
+ Plot simtel event histograms.
17
+
18
+ Parameters
19
+ ----------
20
+ histograms: SimtelIOEventHistograms
21
+ Instance containing the histograms to plot.
22
+ output_path: Path or str, optional
23
+ Directory to save plots. If None, plots will be displayed.
24
+ limits: dict, optional
25
+ Dictionary containing limits for plotting. Keys can include:
26
+ - "upper_radius_limit": Upper limit for core distance
27
+ - "lower_energy_limit": Lower limit for energy
28
+ - "viewcone_radius": Radius for the viewcone
29
+ rebin_factor: int, optional
30
+ Factor by which to reduce the number of bins in 2D histograms for re-binned plots.
31
+ Default is 2 (merge every 2 bins). Set to 0 or 1 to disable re-binning.
32
+ array_name: str, optional
33
+ Name of the telescope array configuration.
34
+ """
35
+ _logger.info(f"Plotting histograms written to {output_path}")
36
+
37
+ plots = _generate_plot_configurations(histograms, limits)
38
+ _execute_plotting_loop(plots, output_path, rebin_factor, array_name)
39
+
40
+
41
+ def _get_limits(name, limits):
42
+ """
43
+ Extract limits from the provided dictionary for plotting.
44
+
45
+ Fine tuned to expected histograms to be plotted.
46
+ """
47
+
48
+ def _safe_value(limits, key):
49
+ val = limits.get(key)
50
+ return getattr(val, "value", None)
51
+
52
+ mapping = {
53
+ "energy": {"x": _safe_value(limits, "lower_energy_limit")},
54
+ "core_distance": {"x": _safe_value(limits, "upper_radius_limit")},
55
+ "angular_distance": {"x": _safe_value(limits, "viewcone_radius")},
56
+ "core_vs_energy": {
57
+ "x": _safe_value(limits, "upper_radius_limit"),
58
+ "y": _safe_value(limits, "lower_energy_limit"),
59
+ },
60
+ "angular_distance_vs_energy": {
61
+ "x": _safe_value(limits, "viewcone_radius"),
62
+ "y": _safe_value(limits, "lower_energy_limit"),
63
+ },
64
+ "x_core_shower_vs_y_core_shower": {"r": _safe_value(limits, "upper_radius_limit")},
65
+ }
66
+ return mapping.get(name)
67
+
68
+
69
+ def _generate_plot_configurations(histograms, limits):
70
+ """Generate plot configurations for all histogram types."""
71
+ hist_1d_params = {"color": "tab:green", "edgecolor": "tab:green", "lw": 1}
72
+ hist_2d_params = {"norm": "log", "cmap": "viridis", "show_contour": False}
73
+ hist_2d_normalized_params = {"norm": "linear", "cmap": "viridis", "show_contour": True}
74
+ plots = {}
75
+ for name, hist in histograms.items():
76
+ if hist["histogram"] is None:
77
+ continue
78
+ if hist["1d"]:
79
+ plots[name] = _create_1d_plot_config(
80
+ hist, name=name, plot_params=hist_1d_params, limits=limits
81
+ )
82
+ else:
83
+ if "cumulative" in name or "efficiency" in name:
84
+ plot_params = hist_2d_normalized_params
85
+ else:
86
+ plot_params = hist_2d_params
87
+
88
+ plots[name] = _create_2d_plot_config(
89
+ hist, name=name, plot_params=plot_params, limits=limits
90
+ )
91
+ return plots
92
+
93
+
94
+ def _get_axis_title(axis_titles, axis):
95
+ """Return axis title for given axis."""
96
+ if axis_titles is None:
97
+ return None
98
+ if axis == "x" and len(axis_titles) > 0:
99
+ return axis_titles[0]
100
+ if axis == "y" and len(axis_titles) > 1:
101
+ return axis_titles[1]
102
+ if axis == "z" and len(axis_titles) > 2:
103
+ return axis_titles[2]
104
+ return None
105
+
106
+
107
+ def _create_1d_plot_config(histogram, name, plot_params, limits):
108
+ """Create a 1D plot configuration."""
109
+ _logger.debug(f"Creating plot config for {name} with params: {plot_params}")
110
+ return {
111
+ "data": histogram["histogram"],
112
+ "bins": histogram["bin_edges"],
113
+ "plot_type": "histogram",
114
+ "plot_params": plot_params,
115
+ "labels": {
116
+ "x": _get_axis_title(histogram.get("axis_titles"), "x"),
117
+ "y": _get_axis_title(histogram.get("axis_titles"), "y"),
118
+ "title": f"{histogram['title']}: {name.replace('_', ' ')}",
119
+ },
120
+ "scales": histogram["plot_scales"],
121
+ "lines": _get_limits(name, limits) if limits else {},
122
+ "filename": name,
123
+ }
124
+
125
+
126
+ def _create_2d_plot_config(histogram, name, plot_params, limits):
127
+ """Create a 2D plot configuration."""
128
+ _logger.debug(f"Creating plot config for {name} with params: {plot_params}")
129
+ return {
130
+ "data": histogram["histogram"],
131
+ "bins": [histogram["bin_edges"][0], histogram["bin_edges"][1]],
132
+ "plot_type": "histogram2d",
133
+ "plot_params": plot_params,
134
+ "labels": {
135
+ "x": _get_axis_title(histogram.get("axis_titles"), "x"),
136
+ "y": _get_axis_title(histogram.get("axis_titles"), "y"),
137
+ "title": f"{histogram['title']}: {name.replace('_', ' ')}",
138
+ },
139
+ "lines": _get_limits(name, limits) if limits else {},
140
+ "scales": histogram["plot_scales"],
141
+ "colorbar_label": _get_axis_title(histogram.get("axis_titles"), "z"),
142
+ "filename": name,
143
+ }
144
+
145
+
146
+ def _execute_plotting_loop(plots, output_path, rebin_factor, array_name):
147
+ """Execute the main plotting loop for all plot configurations."""
148
+ for plot_key, plot_args in plots.items():
149
+ plot_filename = plot_args.pop("filename")
150
+
151
+ if plot_args.get("data") is None:
152
+ _logger.warning(f"Skipping plot {plot_key} - no data available")
153
+ continue
154
+
155
+ if array_name and plot_args.get("labels", {}).get("title"):
156
+ plot_args["labels"]["title"] += f" ({array_name} array)"
157
+
158
+ filename = _build_plot_filename(plot_filename, array_name)
159
+ output_file = output_path / filename if output_path else None
160
+ result = _create_plot(**plot_args, output_file=output_file)
161
+
162
+ # Skip re-binned plot if main plot failed
163
+ if result is None:
164
+ continue
165
+
166
+ if _should_create_rebinned_plot(rebin_factor, plot_args, plot_key):
167
+ _create_rebinned_plot(plot_args, filename, output_path, rebin_factor)
168
+
169
+
170
+ def _build_plot_filename(base_filename, array_name=None):
171
+ """
172
+ Build the full plot filename with appropriate extensions.
173
+
174
+ Parameters
175
+ ----------
176
+ base_filename : str
177
+ The base filename without extension
178
+ array_name : str, optional
179
+ Name of the array to append to filename
180
+
181
+ Returns
182
+ -------
183
+ str
184
+ Complete filename with extension
185
+ """
186
+ return f"{base_filename}_{array_name}.png" if array_name else f"{base_filename}.png"
187
+
188
+
189
+ def _should_create_rebinned_plot(rebin_factor, plot_args, plot_key):
190
+ """
191
+ Check if a re-binned version of the plot should be created.
192
+
193
+ Parameters
194
+ ----------
195
+ rebin_factor : int
196
+ Factor by which to rebin the energy axis
197
+ plot_args : dict
198
+ Plot arguments
199
+ plot_key : str
200
+ Key identifying the plot type
201
+
202
+ Returns
203
+ -------
204
+ bool
205
+ True if a re-binned plot should be created, False otherwise
206
+ """
207
+ return (
208
+ rebin_factor > 1
209
+ and plot_args["plot_type"] == "histogram2d"
210
+ and plot_key.endswith("_cumulative")
211
+ and plot_args.get("plot_params", {}).get("norm") == "linear"
212
+ )
213
+
214
+
215
+ def _create_rebinned_plot(plot_args, filename, output_path, rebin_factor):
216
+ """
217
+ Create a re-binned version of a 2D histogram plot.
218
+
219
+ Parameters
220
+ ----------
221
+ plot_args : dict
222
+ Plot arguments for the original plot
223
+ filename : str
224
+ Filename of the original plot
225
+ output_path : Path or None
226
+ Path to save the plot to, or None
227
+ rebin_factor : int
228
+ Factor by which to rebin the energy axis
229
+ """
230
+ data = plot_args["data"]
231
+ bins = plot_args["bins"]
232
+
233
+ rebinned_data, rebinned_x_bins, rebinned_y_bins = SimtelIOEventHistograms.rebin_2d_histogram(
234
+ data, bins[0], bins[1], rebin_factor
235
+ )
236
+
237
+ rebinned_plot_args = plot_args.copy()
238
+ rebinned_plot_args["data"] = rebinned_data
239
+ rebinned_plot_args["bins"] = [rebinned_x_bins, rebinned_y_bins]
240
+
241
+ if rebinned_plot_args.get("labels", {}).get("title"):
242
+ rebinned_plot_args["labels"]["title"] += f" (Energy rebinned {rebin_factor}x)"
243
+
244
+ rebinned_filename = f"{filename.replace('.png', '')}_rebinned.png"
245
+ rebinned_output_file = output_path / rebinned_filename if output_path else None
246
+ _create_plot(**rebinned_plot_args, output_file=rebinned_output_file)
247
+
248
+
249
+ def _create_plot(
250
+ data,
251
+ bins=None,
252
+ plot_type="histogram",
253
+ plot_params=None,
254
+ labels=None,
255
+ scales=None,
256
+ colorbar_label=None,
257
+ output_file=None,
258
+ lines=None,
259
+ ):
260
+ """Create and save a plot with the given parameters."""
261
+ plot_params = plot_params or {}
262
+ labels = labels or {}
263
+ scales = scales or {}
264
+ lines = lines or {}
265
+
266
+ if not _has_data(data):
267
+ return None
268
+
269
+ fig, ax = plt.subplots(figsize=(8, 6))
270
+ _plot_data(ax, data, bins, plot_type, plot_params, colorbar_label)
271
+ _add_lines(ax, lines)
272
+ ax.set(
273
+ xlabel=labels.get("x", ""),
274
+ ylabel=labels.get("y", ""),
275
+ title=labels.get("title", ""),
276
+ xscale=scales.get("x", "linear"),
277
+ yscale=scales.get("y", "linear"),
278
+ )
279
+ if output_file:
280
+ _logger.info(f"Saving plot to {output_file}")
281
+ fig.savefig(output_file, dpi=300, bbox_inches="tight")
282
+ plt.close(fig)
283
+ else:
284
+ plt.tight_layout()
285
+ plt.show()
286
+
287
+ return fig
288
+
289
+
290
+ def _has_data(data):
291
+ """Check that the data for plotting is not None or empty."""
292
+ if data is None or (isinstance(data, np.ndarray) and data.size == 0):
293
+ _logger.warning("No data available for plotting")
294
+ return False
295
+ return True
296
+
297
+
298
+ def _plot_data(ax, data, bins, plot_type, plot_params, colorbar_label):
299
+ """Plot the data on the given axes."""
300
+ if plot_type == "histogram":
301
+ ax.bar(bins[:-1], data, width=np.diff(bins), **plot_params)
302
+ elif plot_type == "histogram2d":
303
+ pcm = _create_2d_histogram_plot(data, bins, plot_params)
304
+ plt.colorbar(pcm, label=colorbar_label)
305
+
306
+
307
+ def _add_lines(ax, lines):
308
+ """Add reference lines to the plot."""
309
+ if lines.get("x") is not None:
310
+ ax.axvline(lines["x"], color="r", linestyle="--", linewidth=0.5)
311
+ if lines.get("y") is not None:
312
+ ax.axhline(lines["y"], color="r", linestyle="--", linewidth=0.5)
313
+ if lines.get("r") is not None:
314
+ ax.add_artist(
315
+ plt.Circle((0, 0), lines["r"], color="r", fill=False, linestyle="--", linewidth=0.5)
316
+ )
317
+
318
+
319
+ def _create_2d_histogram_plot(data, bins, plot_params):
320
+ """
321
+ Create a 2D histogram plot with the given parameters.
322
+
323
+ Parameters
324
+ ----------
325
+ data : np.ndarray
326
+ 2D histogram data
327
+ bins : tuple of np.ndarray
328
+ Bin edges for x and y axes
329
+ plot_params : dict
330
+ Plot parameters including norm, cmap, and show_contour
331
+
332
+ Returns
333
+ -------
334
+ matplotlib.collections.QuadMesh
335
+ The created pcolormesh object for colorbar attachment
336
+ """
337
+ if plot_params.get("norm") == "linear":
338
+ pcm = plt.pcolormesh(
339
+ bins[0],
340
+ bins[1],
341
+ data.T,
342
+ vmin=0,
343
+ vmax=1,
344
+ cmap=plot_params.get("cmap", "viridis"),
345
+ )
346
+ # Add contour line at value=1.0 for normalized histograms
347
+ if plot_params.get("show_contour", True):
348
+ x_centers = (bins[0][1:] + bins[0][:-1]) / 2
349
+ y_centers = (bins[1][1:] + bins[1][:-1]) / 2
350
+ x_mesh, y_mesh = np.meshgrid(x_centers, y_centers)
351
+ plt.contour(
352
+ x_mesh,
353
+ y_mesh,
354
+ data.T,
355
+ levels=[0.999999], # very close to 1 for floating point precision
356
+ colors=["tab:red"],
357
+ linestyles=["--"],
358
+ linewidths=[0.5],
359
+ )
360
+ else:
361
+ # Handle empty or invalid data for logarithmic scaling
362
+ data_max = data.max()
363
+ if data_max <= 0:
364
+ _logger.warning("No positive data found for logarithmic scaling, using linear scale")
365
+ pcm = plt.pcolormesh(
366
+ bins[0], bins[1], data.T, vmin=0, vmax=max(1, data_max), cmap="viridis"
367
+ )
368
+ else:
369
+ # Ensure vmin is less than vmax for LogNorm
370
+ vmin = max(1, data[data > 0].min()) if np.any(data > 0) else 1
371
+ vmax = max(vmin + 1, data_max)
372
+ pcm = plt.pcolormesh(
373
+ bins[0], bins[1], data.T, norm=LogNorm(vmin=vmin, vmax=vmax), cmap="viridis"
374
+ )
375
+
376
+ return pcm