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.
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -3
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +54 -51
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +3 -3
- simtools/_version.py +2 -2
- simtools/applications/calculate_incident_angles.py +182 -0
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +17 -14
- simtools/applications/db_add_value_from_json_to_db.py +6 -9
- simtools/applications/db_generate_compound_indexes.py +7 -3
- simtools/applications/db_get_file_from_db.py +11 -23
- simtools/applications/derive_psf_parameters.py +58 -39
- simtools/applications/derive_trigger_rates.py +91 -0
- simtools/applications/generate_corsika_histograms.py +7 -184
- simtools/applications/maintain_simulation_model_add_production.py +105 -0
- simtools/applications/plot_simtel_events.py +5 -189
- simtools/applications/print_version.py +8 -7
- simtools/applications/validate_file_using_schema.py +7 -4
- simtools/configuration/commandline_parser.py +17 -11
- simtools/corsika/corsika_histograms.py +81 -0
- simtools/data_model/validate_data.py +8 -3
- simtools/db/db_handler.py +122 -31
- simtools/db/db_model_upload.py +51 -30
- simtools/dependencies.py +10 -5
- simtools/layout/array_layout_utils.py +37 -5
- simtools/model/array_model.py +18 -1
- simtools/model/model_repository.py +118 -63
- simtools/model/site_model.py +25 -0
- simtools/production_configuration/derive_corsika_limits.py +9 -34
- simtools/ray_tracing/incident_angles.py +706 -0
- simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
- simtools/schemas/model_parameter_and_data_schema.metaschema.yml +2 -2
- simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +1 -1
- simtools/schemas/model_parameters/nsb_spectrum.schema.yml +22 -29
- simtools/schemas/model_parameters/stars.schema.yml +1 -1
- simtools/schemas/production_tables.schema.yml +5 -0
- simtools/simtel/simtel_config_writer.py +18 -20
- simtools/simtel/simtel_io_event_histograms.py +253 -516
- simtools/simtel/simtel_io_event_reader.py +51 -2
- simtools/simtel/simtel_io_event_writer.py +31 -11
- simtools/simtel/simtel_io_metadata.py +1 -1
- simtools/simtel/simtel_table_reader.py +3 -3
- simtools/simulator.py +1 -4
- simtools/telescope_trigger_rates.py +119 -0
- simtools/testing/log_inspector.py +13 -11
- simtools/utils/geometry.py +20 -0
- simtools/version.py +89 -0
- simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
- simtools/visualization/plot_incident_angles.py +431 -0
- simtools/visualization/plot_psf.py +673 -0
- simtools/visualization/plot_simtel_event_histograms.py +376 -0
- simtools/visualization/{simtel_event_plots.py → plot_simtel_events.py} +284 -87
- simtools/visualization/visualize.py +1 -3
- simtools/applications/calculate_trigger_rate.py +0 -187
- simtools/applications/generate_sim_telarray_histograms.py +0 -196
- simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
- simtools/simtel/simtel_io_histogram.py +0 -623
- simtools/simtel/simtel_io_histograms.py +0 -556
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.19.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|