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,431 @@
1
+ #!/usr/bin/python3
2
+ """Plot incident angle histograms for focal, primary, and secondary mirrors.
3
+
4
+ Plots the primary-mirror hit radius if available.
5
+ """
6
+
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ import astropy.units as u
11
+ import matplotlib.pyplot as plt
12
+ import numpy as np
13
+
14
+ __all__ = ["plot_incident_angles"]
15
+
16
+ Y_AXIS_BIN_COUNT_LABEL = "Density"
17
+
18
+
19
+ def _gather_angle_arrays(results_by_offset, column, log):
20
+ arrays = []
21
+ for off, tab in results_by_offset.items():
22
+ if tab is None or len(tab) == 0:
23
+ if column == "angle_incidence_focal":
24
+ log.warning(f"Empty results for off-axis={off}")
25
+ continue
26
+ if column not in tab.colnames:
27
+ continue
28
+ arrays.append(tab[column].to(u.deg).value)
29
+ return arrays
30
+
31
+
32
+ def _gather_radius_arrays(results_by_offset, column, log):
33
+ arrays = []
34
+ for off, tab in results_by_offset.items():
35
+ if tab is None or len(tab) == 0 or column not in tab.colnames:
36
+ continue
37
+ try:
38
+ arrays.append(tab[column].to(u.m).value)
39
+ except (AttributeError, ValueError, TypeError):
40
+ log.warning("Skipping radius values for off-axis=%s due to unit/format issue", off)
41
+ return arrays
42
+
43
+
44
+ def _plot_radius_vs_angle(
45
+ results_by_offset,
46
+ radius_col,
47
+ angle_col,
48
+ title,
49
+ out_path,
50
+ log,
51
+ ):
52
+ any_points = False
53
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
54
+ for off in sorted(results_by_offset.keys()):
55
+ tab = results_by_offset[off]
56
+ if tab is None or len(tab) == 0:
57
+ continue
58
+ if radius_col not in tab.colnames or angle_col not in tab.colnames:
59
+ continue
60
+ r = tab[radius_col].to(u.m).value
61
+ a = tab[angle_col].to(u.deg).value
62
+ mask = np.isfinite(r) & np.isfinite(a)
63
+ r, a = r[mask], a[mask]
64
+ if r.size == 0 or a.size == 0:
65
+ continue
66
+ any_points = True
67
+ ax.scatter(r, a, s=4, alpha=0.25, label=f"off-axis {off:g} deg")
68
+ if not any_points:
69
+ plt.close(fig)
70
+ log.warning("No valid data to plot for %s", title)
71
+ return
72
+ ax.set_xlabel("Hit radius (m)")
73
+ ax.set_ylabel("Angle of incidence (deg)")
74
+ ax.set_title(title)
75
+ ax.grid(True, alpha=0.3)
76
+ ax.legend(markerscale=3)
77
+ plt.tight_layout()
78
+ plt.savefig(out_path, dpi=300)
79
+ plt.close(fig)
80
+
81
+
82
+ def _plot_xy_heatmap(
83
+ results_by_offset,
84
+ x_col,
85
+ y_col,
86
+ title,
87
+ out_path,
88
+ log,
89
+ bins=400,
90
+ ):
91
+ any_points = False
92
+ fig, ax = plt.subplots(1, 1, figsize=(6, 5))
93
+ h = None
94
+ for _off, x, y in _iter_xy_valid_points(results_by_offset, x_col, y_col):
95
+ any_points = True
96
+ h = ax.hist2d(x, y, bins=bins, cmap="viridis", norm=None)
97
+ if not any_points:
98
+ plt.close(fig)
99
+ log.warning("No valid data to plot for %s", title)
100
+ return
101
+ ax.set_xlabel("X hit (m)")
102
+ ax.set_ylabel("Y hit (m)")
103
+ ax.set_title(title)
104
+ ax.grid(False)
105
+ cb = plt.colorbar(h[3], ax=ax)
106
+ cb.set_label("Counts per bin")
107
+ plt.tight_layout()
108
+ plt.savefig(out_path, dpi=300)
109
+ plt.close(fig)
110
+
111
+
112
+ def _plot_xy_heatmaps_per_offset(
113
+ results_by_offset,
114
+ x_col,
115
+ y_col,
116
+ title_prefix,
117
+ file_stem,
118
+ out_dir,
119
+ label,
120
+ bins=400,
121
+ ):
122
+ for off, x, y in _iter_xy_valid_points(results_by_offset, x_col, y_col):
123
+ fig, ax = plt.subplots(1, 1, figsize=(6, 5))
124
+ h = ax.hist2d(x, y, bins=bins, cmap="viridis", norm=None)
125
+ ax.set_xlabel("X hit (m)")
126
+ ax.set_ylabel("Y hit (m)")
127
+ ax.set_aspect("equal", adjustable="box")
128
+ ax.set_title(f"{title_prefix} (off-axis {off:g} deg)")
129
+ cb = plt.colorbar(h[3], ax=ax)
130
+ cb.set_label("Counts per bin")
131
+ plt.tight_layout()
132
+ out_path = out_dir / f"{file_stem}{off:g}_{label}.png"
133
+ plt.savefig(out_path, dpi=300)
134
+ plt.close(fig)
135
+
136
+
137
+ def _iter_xy_valid_points(results_by_offset, x_col, y_col):
138
+ """Yield (off, x, y) arrays for valid entries with finite X/Y in meters.
139
+
140
+ Filters out None/empty tables, missing columns, and non-finite rows.
141
+ Offsets are iterated in sorted order.
142
+ """
143
+ for off in sorted(results_by_offset.keys()):
144
+ tab = results_by_offset[off]
145
+ if tab is None or len(tab) == 0:
146
+ continue
147
+ if x_col not in tab.colnames or y_col not in tab.colnames:
148
+ continue
149
+ x = tab[x_col].to(u.m).value
150
+ y = tab[y_col].to(u.m).value
151
+ mask = np.isfinite(x) & np.isfinite(y)
152
+ x, y = x[mask], y[mask]
153
+ if x.size == 0:
154
+ continue
155
+ yield off, x, y
156
+
157
+
158
+ def _compute_bins(all_vals, bin_width, log, context):
159
+ finite_mask = np.isfinite(all_vals)
160
+ if not np.any(finite_mask):
161
+ if context == "focal":
162
+ log.warning("No focal-surface incidence angle values to plot for this telescope type")
163
+ else:
164
+ log.warning("No %s values to plot for this telescope type", context)
165
+ return None
166
+ vals = all_vals[finite_mask]
167
+ vmin = float(np.floor(vals.min() / bin_width) * bin_width)
168
+ vmax = float(np.ceil(vals.max() / bin_width) * bin_width)
169
+ if not np.isfinite(vmin) or not np.isfinite(vmax):
170
+ log.warning("Invalid bin edges for %s: vmin=%s vmax=%s", context, vmin, vmax)
171
+ return None
172
+ if vmax <= vmin:
173
+ vmax = vmin + bin_width
174
+ return np.arange(vmin, vmax + bin_width * 0.5, bin_width)
175
+
176
+
177
+ def _plot_radius_histograms(
178
+ results_by_offset,
179
+ radius_col,
180
+ title,
181
+ xlabel,
182
+ out_path,
183
+ bin_width_m,
184
+ log,
185
+ ):
186
+ arrays = _gather_radius_arrays(results_by_offset, radius_col, log)
187
+ if not arrays:
188
+ return
189
+ all_vals = np.concatenate(arrays)
190
+ bins_m = _compute_bins(all_vals, bin_width=bin_width_m, log=log, context=f"{radius_col}_m")
191
+ if bins_m is None:
192
+ return
193
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
194
+ for off in sorted(results_by_offset.keys()):
195
+ tab = results_by_offset[off]
196
+ if tab is None or len(tab) == 0 or radius_col not in tab.colnames:
197
+ continue
198
+ data = tab[radius_col].to(u.m).value
199
+ data = data[np.isfinite(data)]
200
+ if data.size == 0:
201
+ continue
202
+ _, _, patches = ax.hist(
203
+ data,
204
+ bins=bins_m,
205
+ density=True,
206
+ stacked=True,
207
+ histtype="step",
208
+ linewidth=0.5,
209
+ label=f"off-axis {off:g} deg",
210
+ zorder=3,
211
+ )
212
+ color = patches[0].get_edgecolor() if patches else None
213
+ ax.hist(
214
+ data,
215
+ bins=bins_m,
216
+ density=True,
217
+ stacked=True,
218
+ histtype="stepfilled",
219
+ alpha=0.15,
220
+ color=color,
221
+ edgecolor="none",
222
+ label="_nolegend_",
223
+ zorder=1,
224
+ )
225
+ ax.hist(
226
+ data,
227
+ bins=bins_m,
228
+ density=True,
229
+ stacked=True,
230
+ histtype="step",
231
+ linewidth=0.5,
232
+ color=color,
233
+ label="_nolegend_",
234
+ zorder=4,
235
+ )
236
+ ax.set_xlabel(xlabel)
237
+ ax.set_ylabel(Y_AXIS_BIN_COUNT_LABEL)
238
+ ax.set_title(title)
239
+ ax.grid(True, alpha=0.3)
240
+ ax.legend()
241
+ plt.tight_layout()
242
+ plt.savefig(out_path, dpi=300)
243
+ plt.close(fig)
244
+
245
+
246
+ def _plot_debug_plots(results_by_offset, out_dir, label, radius_bin_width_m, log):
247
+ _plot_radius_histograms(
248
+ results_by_offset,
249
+ radius_col="primary_hit_radius",
250
+ title="Primary mirror hit radius vs off-axis angle",
251
+ xlabel="Primary-hit radius on M1 (m)",
252
+ out_path=out_dir / f"incident_radius_primary_multi_{label}.png",
253
+ bin_width_m=radius_bin_width_m,
254
+ log=log,
255
+ )
256
+ _plot_radius_histograms(
257
+ results_by_offset,
258
+ radius_col="secondary_hit_radius",
259
+ title="Secondary mirror hit radius vs off-axis angle",
260
+ xlabel="Secondary-hit radius on M2 (m)",
261
+ out_path=out_dir / f"incident_radius_secondary_multi_{label}.png",
262
+ bin_width_m=radius_bin_width_m,
263
+ log=log,
264
+ )
265
+
266
+ _plot_radius_vs_angle(
267
+ results_by_offset,
268
+ radius_col="primary_hit_radius",
269
+ angle_col="angle_incidence_primary",
270
+ title="Primary mirror: hit radius vs incidence angle",
271
+ out_path=out_dir / f"incident_primary_radius_vs_angle_multi_{label}.png",
272
+ log=log,
273
+ )
274
+ _plot_radius_vs_angle(
275
+ results_by_offset,
276
+ radius_col="secondary_hit_radius",
277
+ angle_col="angle_incidence_secondary",
278
+ title="Secondary mirror: hit radius vs incidence angle",
279
+ out_path=out_dir / f"incident_secondary_radius_vs_angle_multi_{label}.png",
280
+ log=log,
281
+ )
282
+
283
+ _plot_xy_heatmaps_per_offset(
284
+ results_by_offset,
285
+ x_col="primary_hit_x",
286
+ y_col="primary_hit_y",
287
+ title_prefix="Primary mirror: X-Y hit distribution",
288
+ file_stem="incident_primary_xy_heatmap_off",
289
+ out_dir=out_dir,
290
+ label=label,
291
+ )
292
+ _plot_xy_heatmaps_per_offset(
293
+ results_by_offset,
294
+ x_col="secondary_hit_x",
295
+ y_col="secondary_hit_y",
296
+ title_prefix="Secondary mirror: X-Y hit distribution",
297
+ file_stem="incident_secondary_xy_heatmap_off",
298
+ out_dir=out_dir,
299
+ label=label,
300
+ )
301
+
302
+
303
+ def _plot_overlay_angles(results_by_offset, column, bins, ax, use_zorder):
304
+ for off in sorted(results_by_offset.keys()):
305
+ tab = results_by_offset[off]
306
+ if tab is None or len(tab) == 0 or column not in tab.colnames:
307
+ continue
308
+ data = tab[column].to(u.deg).value
309
+ data = data[np.isfinite(data)]
310
+ if data.size == 0:
311
+ continue
312
+ z1, z2, z3 = (3, 1, 4) if use_zorder else (None, None, None)
313
+ _, _, patches = ax.hist(
314
+ data,
315
+ bins=bins,
316
+ density=True,
317
+ stacked=True,
318
+ histtype="step",
319
+ linewidth=0.5,
320
+ label=f"off-axis {off:g} deg",
321
+ zorder=z1,
322
+ )
323
+ color = patches[0].get_edgecolor() if patches else None
324
+ ax.hist(
325
+ data,
326
+ bins=bins,
327
+ density=True,
328
+ stacked=True,
329
+ histtype="stepfilled",
330
+ alpha=0.15,
331
+ color=color,
332
+ edgecolor="none",
333
+ label="_nolegend_",
334
+ zorder=z2,
335
+ )
336
+ ax.hist(
337
+ data,
338
+ bins=bins,
339
+ density=True,
340
+ stacked=True,
341
+ histtype="step",
342
+ linewidth=0.5,
343
+ color=color,
344
+ label="_nolegend_",
345
+ zorder=z3,
346
+ )
347
+
348
+
349
+ def _plot_component_angles(
350
+ results_by_offset,
351
+ column,
352
+ title_suffix,
353
+ out_path,
354
+ bin_width_deg,
355
+ log,
356
+ ):
357
+ arrays = _gather_angle_arrays(results_by_offset, column, log)
358
+ if not arrays:
359
+ return
360
+ bins = _compute_bins(np.concatenate(arrays), bin_width_deg, log, context=column)
361
+ if bins is None:
362
+ return
363
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
364
+ _plot_overlay_angles(results_by_offset, column, bins, ax, use_zorder=False)
365
+ ax.set_xlabel("Angle of incidence (deg)")
366
+ ax.set_ylabel(Y_AXIS_BIN_COUNT_LABEL)
367
+ ax.set_title(f"Incident angle {title_suffix} vs off-axis angle")
368
+ ax.grid(True, alpha=0.3)
369
+ ax.legend()
370
+ plt.tight_layout()
371
+ plt.savefig(out_path, dpi=300)
372
+ plt.close(fig)
373
+
374
+
375
+ def plot_incident_angles(
376
+ results_by_offset,
377
+ output_dir,
378
+ label,
379
+ bin_width_deg=0.1,
380
+ radius_bin_width_m=0.01,
381
+ debug_plots=False,
382
+ logger=None,
383
+ ):
384
+ """Plot overlaid histograms of focal, primary, secondary angles, and primary hit radius."""
385
+ log = logger or logging.getLogger(__name__)
386
+ if not results_by_offset:
387
+ log.warning("No results provided for multi-offset plot")
388
+ return
389
+
390
+ out_dir = Path(output_dir) / "plots"
391
+ out_dir.mkdir(parents=True, exist_ok=True)
392
+
393
+ # Focal-surface angles
394
+ arrays = _gather_angle_arrays(results_by_offset, "angle_incidence_focal", log)
395
+ if arrays:
396
+ bins = _compute_bins(np.concatenate(arrays), bin_width_deg, log, context="focal")
397
+ if bins is not None:
398
+ fig, ax = plt.subplots(1, 1, figsize=(7, 5))
399
+ _plot_overlay_angles(
400
+ results_by_offset, "angle_incidence_focal", bins, ax, use_zorder=True
401
+ )
402
+ ax.set_xlabel("Angle of incidence at focal surface (deg) w.r.t. optical axis")
403
+ ax.set_ylabel(Y_AXIS_BIN_COUNT_LABEL)
404
+ ax.set_title("Incident angle distribution vs off-axis angle")
405
+ ax.grid(True, alpha=0.3)
406
+ ax.legend()
407
+ plt.tight_layout()
408
+ plt.savefig(out_dir / f"incident_angles_multi_{label}.png", dpi=300)
409
+ plt.close(fig)
410
+
411
+ # Primary and secondary mirror angles
412
+ _plot_component_angles(
413
+ results_by_offset=results_by_offset,
414
+ column="angle_incidence_primary",
415
+ title_suffix="on primary mirror (w.r.t. normal)",
416
+ out_path=out_dir / f"incident_angles_primary_multi_{label}.png",
417
+ bin_width_deg=bin_width_deg,
418
+ log=log,
419
+ )
420
+ _plot_component_angles(
421
+ results_by_offset=results_by_offset,
422
+ column="angle_incidence_secondary",
423
+ title_suffix="on secondary mirror (w.r.t. normal)",
424
+ out_path=out_dir / f"incident_angles_secondary_multi_{label}.png",
425
+ bin_width_deg=bin_width_deg,
426
+ log=log,
427
+ )
428
+
429
+ # Debug plots
430
+ if debug_plots:
431
+ _plot_debug_plots(results_by_offset, out_dir, label, radius_bin_width_m, log)