gammasimtools 0.20.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.20.0.dist-info → gammasimtools-0.21.0.dist-info}/METADATA +1 -1
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.21.0.dist-info}/RECORD +24 -23
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.21.0.dist-info}/entry_points.txt +1 -1
- simtools/_version.py +2 -2
- simtools/applications/db_generate_compound_indexes.py +1 -1
- simtools/applications/derive_psf_parameters.py +58 -39
- simtools/applications/generate_corsika_histograms.py +7 -184
- simtools/applications/maintain_simulation_model_add_production.py +105 -0
- simtools/applications/plot_simtel_events.py +2 -228
- simtools/applications/print_version.py +8 -7
- simtools/corsika/corsika_histograms.py +81 -0
- simtools/db/db_handler.py +45 -11
- simtools/db/db_model_upload.py +40 -14
- simtools/model/model_repository.py +118 -63
- simtools/ray_tracing/psf_parameter_optimisation.py +999 -565
- simtools/simtel/simtel_config_writer.py +1 -1
- simtools/simulator.py +1 -4
- simtools/version.py +89 -0
- simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
- simtools/visualization/plot_psf.py +673 -0
- simtools/visualization/plot_simtel_events.py +284 -87
- simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.21.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.21.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.21.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optical PSF plotting functions for parameter optimization visualization.
|
|
3
|
+
|
|
4
|
+
This module provides plotting functionality for PSF parameter optimization,
|
|
5
|
+
including parameter comparison plots, convergence plots, and D80 vs off-axis plots.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import astropy.units as u
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
from matplotlib.backends.backend_pdf import PdfPages
|
|
14
|
+
|
|
15
|
+
from simtools.ray_tracing.ray_tracing import RayTracing
|
|
16
|
+
from simtools.visualization import visualize
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
RADIUS_CM = "Radius [cm]"
|
|
22
|
+
CUMULATIVE_PSF = "Cumulative PSF"
|
|
23
|
+
D80_CM_LABEL = "D80 (cm)"
|
|
24
|
+
MAX_OFFSET_DEFAULT = 4.5 # Maximum off-axis angle in degrees
|
|
25
|
+
OFFSET_STEPS_DEFAULT = 0.1 # Step size for off-axis angle sampling
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_significance_label(p_value):
|
|
29
|
+
"""Get significance label for p-value."""
|
|
30
|
+
if p_value > 0.05: # null hypothesis not rejected at the 95% level
|
|
31
|
+
return "GOOD"
|
|
32
|
+
if p_value > 0.01: # null hypothesis rejected at 95% but not at 99% level
|
|
33
|
+
return "FAIR"
|
|
34
|
+
return "POOR" # null hypothesis rejected at 99% level
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_metric_text(d80, metric, p_value=None, use_ks_statistic=False, second_metric=None):
|
|
38
|
+
"""
|
|
39
|
+
Format metric text for display in plots.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
d80 : float
|
|
44
|
+
D80 value
|
|
45
|
+
metric : float
|
|
46
|
+
Primary metric value (RMSD or KS statistic)
|
|
47
|
+
p_value : float, optional
|
|
48
|
+
P-value from KS test
|
|
49
|
+
use_ks_statistic : bool
|
|
50
|
+
If True, metric is KS statistic; if False, metric is RMSD
|
|
51
|
+
second_metric : float, optional
|
|
52
|
+
Second metric value to display alongside the primary metric
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
str
|
|
57
|
+
Formatted metric text
|
|
58
|
+
"""
|
|
59
|
+
d80_text = f"D80 = {d80:.5f} cm"
|
|
60
|
+
|
|
61
|
+
# Create metric text based on the optimization method
|
|
62
|
+
if second_metric is not None:
|
|
63
|
+
# Special case: show both RMSD and KS statistic (for final best plot)
|
|
64
|
+
metric_text = f"RMSD = {metric:.4f}" # metric is RMSD in this case
|
|
65
|
+
metric_text += f"\nKS statistic = {second_metric:.4f}" # second_metric is KS statistic
|
|
66
|
+
if p_value is not None:
|
|
67
|
+
metric_text += f"\np-value = {p_value:.4f}"
|
|
68
|
+
elif use_ks_statistic:
|
|
69
|
+
metric_text = f"KS stat = {metric:.6f}"
|
|
70
|
+
if p_value is not None:
|
|
71
|
+
significance = get_significance_label(p_value)
|
|
72
|
+
metric_text += f"\np-value = {p_value:.6f} ({significance})"
|
|
73
|
+
else:
|
|
74
|
+
metric_text = f"RMSD = {metric:.4f}"
|
|
75
|
+
|
|
76
|
+
return f"{d80_text}\n{metric_text}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _create_base_plot_figure(data_to_plot, simulated_data=None):
|
|
80
|
+
"""
|
|
81
|
+
Create base figure for PSF parameter plots.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
data_to_plot : dict
|
|
86
|
+
Data dictionary for plotting
|
|
87
|
+
simulated_data : array, optional
|
|
88
|
+
Simulated data to add to the plot
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
tuple
|
|
93
|
+
(fig, ax) - figure and axis objects
|
|
94
|
+
"""
|
|
95
|
+
plot_data = data_to_plot.copy()
|
|
96
|
+
|
|
97
|
+
if simulated_data is not None:
|
|
98
|
+
plot_data["simulated"] = simulated_data
|
|
99
|
+
|
|
100
|
+
fig = visualize.plot_1d(
|
|
101
|
+
plot_data,
|
|
102
|
+
plot_difference=True,
|
|
103
|
+
no_markers=True,
|
|
104
|
+
)
|
|
105
|
+
ax = fig.get_axes()[0]
|
|
106
|
+
ax.set_ylim(0, 1.05)
|
|
107
|
+
ax.set_ylabel(CUMULATIVE_PSF)
|
|
108
|
+
return fig, ax
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_parameter_title(pars, is_best):
|
|
112
|
+
"""Build parameter title string for plots."""
|
|
113
|
+
title_prefix = "* " if is_best else ""
|
|
114
|
+
return (
|
|
115
|
+
f"{title_prefix}reflection = "
|
|
116
|
+
f"{pars['mirror_reflection_random_angle'][0]:.5f}, "
|
|
117
|
+
f"{pars['mirror_reflection_random_angle'][1]:.5f}, "
|
|
118
|
+
f"{pars['mirror_reflection_random_angle'][2]:.5f}\n"
|
|
119
|
+
f"align_vertical = {pars['mirror_align_random_vertical'][0]:.5f}, "
|
|
120
|
+
f"{pars['mirror_align_random_vertical'][1]:.5f}, "
|
|
121
|
+
f"{pars['mirror_align_random_vertical'][2]:.5f}, "
|
|
122
|
+
f"{pars['mirror_align_random_vertical'][3]:.5f}\n"
|
|
123
|
+
f"align_horizontal = {pars['mirror_align_random_horizontal'][0]:.5f}, "
|
|
124
|
+
f"{pars['mirror_align_random_horizontal'][1]:.5f}, "
|
|
125
|
+
f"{pars['mirror_align_random_horizontal'][2]:.5f}, "
|
|
126
|
+
f"{pars['mirror_align_random_horizontal'][3]:.5f}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _add_metric_text_box(ax, metrics_text, is_best):
|
|
131
|
+
"""Add metric text box to plot."""
|
|
132
|
+
d80_color = "red" if is_best else "black"
|
|
133
|
+
d80_weight = "bold" if is_best else "normal"
|
|
134
|
+
|
|
135
|
+
ax.text(
|
|
136
|
+
0.5,
|
|
137
|
+
0.3,
|
|
138
|
+
metrics_text,
|
|
139
|
+
verticalalignment="center",
|
|
140
|
+
horizontalalignment="left",
|
|
141
|
+
transform=ax.transAxes,
|
|
142
|
+
color=d80_color,
|
|
143
|
+
weight=d80_weight,
|
|
144
|
+
bbox={"boxstyle": "round,pad=0.3", "facecolor": "yellow", "alpha": 0.7}
|
|
145
|
+
if is_best
|
|
146
|
+
else None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _add_plot_annotations(
|
|
151
|
+
ax, fig, pars, d80, metric, is_best, p_value=None, use_ks_statistic=False, second_metric=None
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
Add title, text annotations, and best parameter indicators to plot.
|
|
155
|
+
|
|
156
|
+
Parameters
|
|
157
|
+
----------
|
|
158
|
+
ax : matplotlib.axes.Axes
|
|
159
|
+
The plot axes
|
|
160
|
+
fig : matplotlib.figure.Figure
|
|
161
|
+
The plot figure
|
|
162
|
+
pars : dict
|
|
163
|
+
Parameter set dictionary
|
|
164
|
+
d80 : float
|
|
165
|
+
D80 value
|
|
166
|
+
metric : float
|
|
167
|
+
Primary metric value
|
|
168
|
+
is_best : bool
|
|
169
|
+
Whether this is the best parameter set
|
|
170
|
+
p_value : float, optional
|
|
171
|
+
P-value from KS test
|
|
172
|
+
use_ks_statistic : bool
|
|
173
|
+
If True, metric is KS statistic; if False, metric is RMSD
|
|
174
|
+
second_metric : float, optional
|
|
175
|
+
Second metric value to display
|
|
176
|
+
"""
|
|
177
|
+
title = _build_parameter_title(pars, is_best)
|
|
178
|
+
ax.set_title(title)
|
|
179
|
+
|
|
180
|
+
metrics_text = _format_metric_text(d80, metric, p_value, use_ks_statistic, second_metric)
|
|
181
|
+
_add_metric_text_box(ax, metrics_text, is_best)
|
|
182
|
+
|
|
183
|
+
if is_best:
|
|
184
|
+
fig.text(
|
|
185
|
+
0.02,
|
|
186
|
+
0.02,
|
|
187
|
+
"* Best parameter set (lowest RMSD)",
|
|
188
|
+
fontsize=8,
|
|
189
|
+
style="italic",
|
|
190
|
+
color="red",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def create_psf_parameter_plot(
|
|
195
|
+
data_to_plot,
|
|
196
|
+
pars,
|
|
197
|
+
d80,
|
|
198
|
+
metric,
|
|
199
|
+
is_best,
|
|
200
|
+
pdf_pages,
|
|
201
|
+
p_value=None,
|
|
202
|
+
use_ks_statistic=False,
|
|
203
|
+
second_metric=None,
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Create a plot for PSF simulation results.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
data_to_plot : dict
|
|
211
|
+
Data dictionary for plotting.
|
|
212
|
+
pars : dict
|
|
213
|
+
Parameter set dictionary.
|
|
214
|
+
d80 : float
|
|
215
|
+
D80 value.
|
|
216
|
+
metric : float
|
|
217
|
+
RMSD value (if use_ks_statistic=False) or KS statistic (if use_ks_statistic=True).
|
|
218
|
+
is_best : bool
|
|
219
|
+
Whether this is the best parameter set.
|
|
220
|
+
pdf_pages : PdfPages
|
|
221
|
+
PDF pages object for saving plots.
|
|
222
|
+
p_value : float, optional
|
|
223
|
+
P-value from KS test (only used when use_ks_statistic=True).
|
|
224
|
+
use_ks_statistic : bool, optional
|
|
225
|
+
If True, metric is KS statistic; if False, metric is RMSD.
|
|
226
|
+
second_metric : float, optional
|
|
227
|
+
Second metric value to display alongside the primary metric (for final best plot).
|
|
228
|
+
"""
|
|
229
|
+
fig, ax = _create_base_plot_figure(data_to_plot)
|
|
230
|
+
|
|
231
|
+
_add_plot_annotations(
|
|
232
|
+
ax, fig, pars, d80, metric, is_best, p_value, use_ks_statistic, second_metric
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
pdf_pages.savefig(fig, bbox_inches="tight")
|
|
236
|
+
plt.clf()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_detailed_parameter_plot(
|
|
240
|
+
pars, ks_statistic, d80, simulated_data, data_to_plot, is_best, pdf_pages, p_value=None
|
|
241
|
+
):
|
|
242
|
+
"""
|
|
243
|
+
Create a detailed plot for a parameter set showing all parameter values.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
pars : dict
|
|
248
|
+
Parameter set dictionary
|
|
249
|
+
ks_statistic : float
|
|
250
|
+
KS statistic value for this parameter set
|
|
251
|
+
d80 : float
|
|
252
|
+
D80 value for this parameter set
|
|
253
|
+
simulated_data : array
|
|
254
|
+
Simulated data for plotting
|
|
255
|
+
data_to_plot : dict
|
|
256
|
+
Data dictionary for plotting
|
|
257
|
+
is_best : bool
|
|
258
|
+
Whether this is the best parameter set
|
|
259
|
+
pdf_pages : PdfPages
|
|
260
|
+
PDF pages object to save the plot
|
|
261
|
+
p_value : float, optional
|
|
262
|
+
P-value from KS test for statistical significance
|
|
263
|
+
"""
|
|
264
|
+
# Check if we have valid simulated data for plotting
|
|
265
|
+
if simulated_data is None:
|
|
266
|
+
logger.warning(
|
|
267
|
+
"No simulated data available for plotting this parameter set, skipping plot creation"
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
fig, ax = _create_base_plot_figure(data_to_plot, simulated_data)
|
|
273
|
+
except (ValueError, RuntimeError, KeyError, TypeError) as e:
|
|
274
|
+
logger.error(f"Failed to create plot for parameters: {e}")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
_add_plot_annotations(
|
|
278
|
+
ax,
|
|
279
|
+
fig,
|
|
280
|
+
pars,
|
|
281
|
+
d80,
|
|
282
|
+
ks_statistic,
|
|
283
|
+
is_best,
|
|
284
|
+
p_value,
|
|
285
|
+
use_ks_statistic=True,
|
|
286
|
+
second_metric=None,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
pdf_pages.savefig(fig, bbox_inches="tight")
|
|
290
|
+
plt.clf()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def create_parameter_progression_plots(results, best_pars, data_to_plot, pdf_pages):
|
|
294
|
+
"""
|
|
295
|
+
Create plots for all parameter sets showing optimization progression.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
results : list
|
|
300
|
+
List of (pars, ks_statistic, p_value, d80, simulated_data) tuples
|
|
301
|
+
best_pars : dict
|
|
302
|
+
Best parameter set for highlighting
|
|
303
|
+
data_to_plot : dict
|
|
304
|
+
Data dictionary for plotting
|
|
305
|
+
pdf_pages : PdfPages
|
|
306
|
+
PDF pages object to save plots
|
|
307
|
+
"""
|
|
308
|
+
logger.info("Creating plots for all parameter sets...")
|
|
309
|
+
|
|
310
|
+
for i, (pars, ks_statistic, p_value, d80, simulated_data) in enumerate(results):
|
|
311
|
+
if simulated_data is None:
|
|
312
|
+
logger.warning(f"No simulated data for iteration {i}, skipping plot")
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
is_best = pars is best_pars
|
|
316
|
+
logger.info(f"Creating plot {i + 1}/{len(results)}{' (BEST)' if is_best else ''}")
|
|
317
|
+
|
|
318
|
+
create_detailed_parameter_plot(
|
|
319
|
+
pars, ks_statistic, d80, simulated_data, data_to_plot, is_best, pdf_pages, p_value
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def create_gradient_descent_convergence_plot(
|
|
324
|
+
gd_results, threshold, output_file, use_ks_statistic=False
|
|
325
|
+
):
|
|
326
|
+
"""
|
|
327
|
+
Create convergence plot showing optimization metric and D80 progression during gradient descent.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
gd_results : list
|
|
332
|
+
List of (params, metric, p_value, d80, simulated_data) tuples from gradient descent
|
|
333
|
+
threshold : float
|
|
334
|
+
Optimization metric threshold used for convergence
|
|
335
|
+
output_file : Path
|
|
336
|
+
Output file path for saving the plot
|
|
337
|
+
use_ks_statistic : bool
|
|
338
|
+
Whether to use KS statistic or RMSD labels and titles
|
|
339
|
+
"""
|
|
340
|
+
logger.info("Creating gradient descent convergence plot...")
|
|
341
|
+
|
|
342
|
+
# Check if results include p-values (for KS statistic mode)
|
|
343
|
+
has_p_values = len(gd_results[0]) >= 4 and gd_results[0][2] is not None
|
|
344
|
+
|
|
345
|
+
if has_p_values and use_ks_statistic:
|
|
346
|
+
_, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12), tight_layout=True)
|
|
347
|
+
else:
|
|
348
|
+
_, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), tight_layout=True)
|
|
349
|
+
|
|
350
|
+
iterations = list(range(len(gd_results)))
|
|
351
|
+
metrics = [r[1] for r in gd_results]
|
|
352
|
+
d80s = [r[3] for r in gd_results]
|
|
353
|
+
|
|
354
|
+
metric_name = "KS Statistic" if use_ks_statistic else "RMSD"
|
|
355
|
+
|
|
356
|
+
# Plot optimization metric progression
|
|
357
|
+
ax1.plot(iterations, metrics, "b.-", linewidth=2, markersize=6)
|
|
358
|
+
ax1.axhline(y=threshold, color="r", linestyle="--", label=f"Threshold: {threshold}")
|
|
359
|
+
ax1.set_xlabel("Iteration")
|
|
360
|
+
ax1.set_ylabel(metric_name)
|
|
361
|
+
ax1.set_title(f"Gradient Descent Convergence - {metric_name}")
|
|
362
|
+
ax1.grid(True, alpha=0.3)
|
|
363
|
+
ax1.legend()
|
|
364
|
+
|
|
365
|
+
# Plot D80 progression
|
|
366
|
+
ax2.plot(iterations, d80s, "g.-", linewidth=2, markersize=6)
|
|
367
|
+
ax2.set_xlabel("Iteration")
|
|
368
|
+
ax2.set_ylabel(D80_CM_LABEL)
|
|
369
|
+
ax2.set_title("Gradient Descent Convergence - D80")
|
|
370
|
+
ax2.grid(True, alpha=0.3)
|
|
371
|
+
|
|
372
|
+
# Plot p-value progression if available and using KS statistic
|
|
373
|
+
if has_p_values and use_ks_statistic:
|
|
374
|
+
p_values = [r[2] for r in gd_results if r[2] is not None]
|
|
375
|
+
p_iterations = [i for i, r in enumerate(gd_results) if r[2] is not None]
|
|
376
|
+
|
|
377
|
+
ax3.plot(p_iterations, p_values, "m.-", linewidth=2, markersize=6)
|
|
378
|
+
ax3.axhline(
|
|
379
|
+
y=0.05, color="orange", linestyle="--", alpha=0.7, label="p = 0.05 (significance)"
|
|
380
|
+
)
|
|
381
|
+
ax3.axhline(
|
|
382
|
+
y=0.01, color="r", linestyle="--", alpha=0.7, label="p = 0.01 (high significance)"
|
|
383
|
+
)
|
|
384
|
+
ax3.set_xlabel("Iteration")
|
|
385
|
+
ax3.set_ylabel("p-value")
|
|
386
|
+
ax3.set_title("Gradient Descent Convergence - Statistical Significance")
|
|
387
|
+
ax3.set_yscale("log") # Log scale for p-values
|
|
388
|
+
ax3.grid(True, alpha=0.3)
|
|
389
|
+
ax3.legend()
|
|
390
|
+
|
|
391
|
+
plt.savefig(output_file, bbox_inches="tight")
|
|
392
|
+
plt.close()
|
|
393
|
+
|
|
394
|
+
logger.info(f"Convergence plot saved to {output_file}")
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def create_monte_carlo_uncertainty_plot(mc_results, output_file, use_ks_statistic=False):
|
|
398
|
+
"""
|
|
399
|
+
Create uncertainty analysis plots showing optimization metric and p-value distributions.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
mc_results : tuple
|
|
404
|
+
Results from Monte Carlo analysis: (mean_metric, std_metric, metric_values,
|
|
405
|
+
mean_p, std_p, p_values, mean_d80, std_d80, d80_values)
|
|
406
|
+
output_file : Path
|
|
407
|
+
Output file path for saving the plot
|
|
408
|
+
use_ks_statistic : bool, optional
|
|
409
|
+
Whether KS statistic mode is being used (affects filename suffix)
|
|
410
|
+
"""
|
|
411
|
+
(
|
|
412
|
+
mean_metric,
|
|
413
|
+
std_metric,
|
|
414
|
+
metric_values,
|
|
415
|
+
mean_p_value,
|
|
416
|
+
_, # std_p_value (unused)
|
|
417
|
+
p_values,
|
|
418
|
+
mean_d80,
|
|
419
|
+
std_d80,
|
|
420
|
+
d80_values,
|
|
421
|
+
) = mc_results
|
|
422
|
+
|
|
423
|
+
logger.info("Creating Monte Carlo uncertainty analysis plot...")
|
|
424
|
+
|
|
425
|
+
# Check if we have valid p-values to determine if this is KS statistic or RMSD mode
|
|
426
|
+
valid_p_values = [p for p in p_values if p is not None] if p_values else []
|
|
427
|
+
is_ks_mode = len(valid_p_values) > 0
|
|
428
|
+
|
|
429
|
+
# Create subplot layout based on mode
|
|
430
|
+
if is_ks_mode:
|
|
431
|
+
# KS mode: 2x2 layout with all 4 plots
|
|
432
|
+
_, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 10), tight_layout=True)
|
|
433
|
+
else:
|
|
434
|
+
# RMSD mode: 1x2 layout with only metric and D80 plots
|
|
435
|
+
_, (ax1, ax3) = plt.subplots(1, 2, figsize=(12, 5), tight_layout=True)
|
|
436
|
+
|
|
437
|
+
# Metric histogram (KS statistic or RMSD)
|
|
438
|
+
metric_name = "KS Statistic" if is_ks_mode else "RMSD"
|
|
439
|
+
ax1.hist(metric_values, bins=20, alpha=0.7, color="blue", edgecolor="black")
|
|
440
|
+
ax1.axvline(
|
|
441
|
+
mean_metric, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean_metric:.6f}"
|
|
442
|
+
)
|
|
443
|
+
ax1.axvline(
|
|
444
|
+
mean_metric - std_metric,
|
|
445
|
+
color="orange",
|
|
446
|
+
linestyle=":",
|
|
447
|
+
alpha=0.7,
|
|
448
|
+
label=f"$\\sigma$: {std_metric:.6f}",
|
|
449
|
+
)
|
|
450
|
+
ax1.axvline(mean_metric + std_metric, color="orange", linestyle=":", alpha=0.7)
|
|
451
|
+
ax1.set_xlabel(metric_name)
|
|
452
|
+
ax1.set_ylabel("Counts")
|
|
453
|
+
ax1.set_title(f"{metric_name} Distribution")
|
|
454
|
+
ax1.legend()
|
|
455
|
+
ax1.grid(True, alpha=0.3)
|
|
456
|
+
|
|
457
|
+
# p-value histogram (only for KS statistic mode)
|
|
458
|
+
if is_ks_mode:
|
|
459
|
+
ax2.hist(valid_p_values, bins=20, alpha=0.7, color="magenta", edgecolor="black")
|
|
460
|
+
if mean_p_value is not None:
|
|
461
|
+
ax2.axvline(
|
|
462
|
+
mean_p_value,
|
|
463
|
+
color="red",
|
|
464
|
+
linestyle="--",
|
|
465
|
+
linewidth=2,
|
|
466
|
+
label=f"Mean: {mean_p_value:.6f}",
|
|
467
|
+
)
|
|
468
|
+
ax2.axvline(0.05, color="orange", linestyle="--", alpha=0.7, label="p = 0.05")
|
|
469
|
+
ax2.axvline(0.01, color="red", linestyle="--", alpha=0.7, label="p = 0.01")
|
|
470
|
+
ax2.set_xlabel("p-value")
|
|
471
|
+
ax2.set_ylabel("Counts")
|
|
472
|
+
ax2.set_title("p-value Distribution")
|
|
473
|
+
ax2.legend()
|
|
474
|
+
ax2.grid(True, alpha=0.3)
|
|
475
|
+
|
|
476
|
+
# D80 histogram
|
|
477
|
+
ax3.hist(d80_values, bins=20, alpha=0.7, color="green", edgecolor="black")
|
|
478
|
+
ax3.axvline(
|
|
479
|
+
mean_d80, color="red", linestyle="--", linewidth=2, label=f"Mean: {mean_d80:.4f} cm"
|
|
480
|
+
)
|
|
481
|
+
ax3.axvline(
|
|
482
|
+
mean_d80 - std_d80,
|
|
483
|
+
color="orange",
|
|
484
|
+
linestyle=":",
|
|
485
|
+
alpha=0.7,
|
|
486
|
+
label=f"$\\sigma$: {std_d80:.4f} cm",
|
|
487
|
+
)
|
|
488
|
+
ax3.axvline(mean_d80 + std_d80, color="orange", linestyle=":", alpha=0.7)
|
|
489
|
+
ax3.set_xlabel(D80_CM_LABEL)
|
|
490
|
+
ax3.set_ylabel("Counts")
|
|
491
|
+
ax3.set_title("D80 Distribution")
|
|
492
|
+
ax3.legend()
|
|
493
|
+
ax3.grid(True, alpha=0.3)
|
|
494
|
+
|
|
495
|
+
# Scatter plot: Metric vs p-value (only for KS statistic mode)
|
|
496
|
+
if is_ks_mode:
|
|
497
|
+
ax4.scatter(metric_values, valid_p_values, alpha=0.6, color="purple")
|
|
498
|
+
ax4.axhline(y=0.05, color="orange", linestyle="--", alpha=0.7, label="p = 0.05")
|
|
499
|
+
ax4.axhline(y=0.01, color="red", linestyle="--", alpha=0.7, label="p = 0.01")
|
|
500
|
+
ax4.set_xlabel(metric_name)
|
|
501
|
+
ax4.set_ylabel("p-value")
|
|
502
|
+
ax4.set_title(f"{metric_name} vs p-value Correlation")
|
|
503
|
+
ax4.set_yscale("log")
|
|
504
|
+
ax4.legend()
|
|
505
|
+
ax4.grid(True, alpha=0.3)
|
|
506
|
+
|
|
507
|
+
# Save plot in both PDF and PNG formats with appropriate suffix
|
|
508
|
+
suffix = "_ks" if use_ks_statistic else "_rmsd"
|
|
509
|
+
|
|
510
|
+
# Generate base filename without extension
|
|
511
|
+
base_path = output_file.with_suffix("")
|
|
512
|
+
base_name = str(base_path)
|
|
513
|
+
|
|
514
|
+
# Add suffix and save in both formats
|
|
515
|
+
pdf_file = f"{base_name}{suffix}.pdf"
|
|
516
|
+
png_file = f"{base_name}{suffix}.png"
|
|
517
|
+
|
|
518
|
+
plt.savefig(pdf_file, bbox_inches="tight")
|
|
519
|
+
plt.savefig(png_file, bbox_inches="tight", dpi=150)
|
|
520
|
+
plt.close()
|
|
521
|
+
|
|
522
|
+
logger.info(f"Monte Carlo uncertainty plot saved to {pdf_file}")
|
|
523
|
+
logger.info(f"Monte Carlo uncertainty plot saved to {png_file}")
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def create_d80_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, output_dir):
|
|
527
|
+
"""
|
|
528
|
+
Create D80 vs off-axis angle plot using the best parameters.
|
|
529
|
+
|
|
530
|
+
Parameters
|
|
531
|
+
----------
|
|
532
|
+
tel_model : TelescopeModel
|
|
533
|
+
Telescope model object.
|
|
534
|
+
site_model : SiteModel
|
|
535
|
+
Site model object.
|
|
536
|
+
args_dict : dict
|
|
537
|
+
Dictionary containing parsed command-line arguments.
|
|
538
|
+
best_pars : dict
|
|
539
|
+
Best parameter set.
|
|
540
|
+
output_dir : Path
|
|
541
|
+
Output directory for saving plots.
|
|
542
|
+
"""
|
|
543
|
+
logger.info("Creating D80 vs off-axis angle plot with best parameters...")
|
|
544
|
+
|
|
545
|
+
# Apply best parameters to telescope model
|
|
546
|
+
tel_model.change_multiple_parameters(**best_pars)
|
|
547
|
+
|
|
548
|
+
# Create off-axis angle array
|
|
549
|
+
max_offset = args_dict.get("max_offset", MAX_OFFSET_DEFAULT)
|
|
550
|
+
offset_steps = args_dict.get("offset_steps", OFFSET_STEPS_DEFAULT)
|
|
551
|
+
off_axis_angles = np.linspace(
|
|
552
|
+
0,
|
|
553
|
+
max_offset,
|
|
554
|
+
int(max_offset / offset_steps) + 1,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
ray = RayTracing(
|
|
558
|
+
telescope_model=tel_model,
|
|
559
|
+
site_model=site_model,
|
|
560
|
+
simtel_path=args_dict["simtel_path"],
|
|
561
|
+
zenith_angle=args_dict["zenith"] * u.deg,
|
|
562
|
+
source_distance=args_dict["src_distance"] * u.km,
|
|
563
|
+
off_axis_angle=off_axis_angles * u.deg,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
logger.info(f"Running ray tracing for {len(off_axis_angles)} off-axis angles...")
|
|
567
|
+
ray.simulate(test=args_dict.get("test", False), force=True)
|
|
568
|
+
ray.analyze(force=True)
|
|
569
|
+
|
|
570
|
+
for key in ["d80_cm", "d80_deg"]:
|
|
571
|
+
plt.figure(figsize=(10, 6), tight_layout=True)
|
|
572
|
+
|
|
573
|
+
ray.plot(key, marker="o", linestyle="-", color="blue", linewidth=2, markersize=6)
|
|
574
|
+
|
|
575
|
+
parameters_text = (
|
|
576
|
+
f"Best Parameters: \n"
|
|
577
|
+
f"reflection=["
|
|
578
|
+
f"{', '.join(f'{x:.4f}' for x in best_pars['mirror_reflection_random_angle'])}],\n"
|
|
579
|
+
f"align_horizontal=["
|
|
580
|
+
f"{', '.join(f'{x:.4f}' for x in best_pars['mirror_align_random_horizontal'])}]\n"
|
|
581
|
+
f"align_vertical=["
|
|
582
|
+
f"{', '.join(f'{x:.4f}' for x in best_pars['mirror_align_random_vertical'])}]\n"
|
|
583
|
+
)
|
|
584
|
+
plt.title(parameters_text)
|
|
585
|
+
plt.xlabel("Off-axis Angle (degrees)")
|
|
586
|
+
plt.ylabel(D80_CM_LABEL if key == "d80_cm" else "D80 (degrees)")
|
|
587
|
+
plt.ylim(bottom=0)
|
|
588
|
+
plt.xticks(rotation=45)
|
|
589
|
+
plt.xlim(0, max_offset)
|
|
590
|
+
plt.grid(True, alpha=0.3)
|
|
591
|
+
|
|
592
|
+
plot_file_name = f"{tel_model.name}_best_params_{key}.png"
|
|
593
|
+
plot_file = output_dir.joinpath(plot_file_name)
|
|
594
|
+
visualize.save_figure(
|
|
595
|
+
plt, plot_file, figure_format=["png"], log_title=f"D80 vs off-axis ({key})"
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
plt.close("all")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def setup_pdf_plotting(args_dict, output_dir, tel_model_name):
|
|
602
|
+
"""
|
|
603
|
+
Set up PDF plotting for gradient descent optimization if requested.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
args_dict : dict
|
|
608
|
+
Dictionary containing command-line arguments with plot_all flag.
|
|
609
|
+
output_dir : Path
|
|
610
|
+
Directory where the PDF file will be saved.
|
|
611
|
+
tel_model_name : str
|
|
612
|
+
Name of the telescope model for filename generation.
|
|
613
|
+
|
|
614
|
+
Returns
|
|
615
|
+
-------
|
|
616
|
+
PdfPages or None
|
|
617
|
+
PdfPages object for saving plots if plotting is requested, None otherwise.
|
|
618
|
+
|
|
619
|
+
Notes
|
|
620
|
+
-----
|
|
621
|
+
Creates a PDF file for saving cumulative PSF plots during gradient descent
|
|
622
|
+
optimization. Returns None if plotting is not requested via the plot_all flag.
|
|
623
|
+
"""
|
|
624
|
+
if not args_dict.get("plot_all", False):
|
|
625
|
+
return None
|
|
626
|
+
pdf_filename = output_dir / f"psf_gradient_descent_plots_{tel_model_name}.pdf"
|
|
627
|
+
pdf_pages = PdfPages(pdf_filename)
|
|
628
|
+
logger.info(f"Creating cumulative PSF plots for each iteration (saving to {pdf_filename})")
|
|
629
|
+
return pdf_pages
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def create_optimization_plots(args_dict, gd_results, tel_model, data_to_plot, output_dir):
|
|
633
|
+
"""
|
|
634
|
+
Create optimization plots for all iterations if requested.
|
|
635
|
+
|
|
636
|
+
Parameters
|
|
637
|
+
----------
|
|
638
|
+
args_dict : dict
|
|
639
|
+
Dictionary containing command-line arguments with save_plots flag.
|
|
640
|
+
gd_results : list
|
|
641
|
+
List of (params, rmsd, _, d80, _) tuples from gradient descent optimization.
|
|
642
|
+
tel_model : TelescopeModel
|
|
643
|
+
Telescope model object for naming files.
|
|
644
|
+
data_to_plot : dict
|
|
645
|
+
Dictionary containing measured PSF data.
|
|
646
|
+
output_dir : Path
|
|
647
|
+
Directory where the PDF file will be saved.
|
|
648
|
+
|
|
649
|
+
Notes
|
|
650
|
+
-----
|
|
651
|
+
Creates a PDF file with plots for optimization iterations. Only creates plots
|
|
652
|
+
every 5 iterations plus the final iteration to avoid excessively large files.
|
|
653
|
+
Returns early if save_plots flag is not set.
|
|
654
|
+
"""
|
|
655
|
+
if not args_dict.get("save_plots", False):
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
pdf_filename = output_dir.joinpath(f"psf_optimization_results_{tel_model.name}.pdf")
|
|
659
|
+
pdf_pages = PdfPages(pdf_filename)
|
|
660
|
+
logger.info(f"Creating PSF plots for each optimization iteration (saving to {pdf_filename})")
|
|
661
|
+
|
|
662
|
+
for i, (params, rmsd, _, d80, _) in enumerate(gd_results):
|
|
663
|
+
if i % 5 == 0 or i == len(gd_results) - 1:
|
|
664
|
+
create_psf_parameter_plot(
|
|
665
|
+
data_to_plot,
|
|
666
|
+
params,
|
|
667
|
+
d80,
|
|
668
|
+
rmsd,
|
|
669
|
+
is_best=(i == len(gd_results) - 1),
|
|
670
|
+
pdf_pages=pdf_pages,
|
|
671
|
+
use_ks_statistic=False,
|
|
672
|
+
)
|
|
673
|
+
pdf_pages.close()
|