gammasimtools 0.24.0__py3-none-any.whl → 0.25.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.24.0.dist-info → gammasimtools-0.25.0.dist-info}/METADATA +1 -1
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/RECORD +58 -55
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/entry_points.txt +1 -0
- simtools/_version.py +2 -2
- simtools/application_control.py +50 -0
- simtools/applications/derive_psf_parameters.py +5 -0
- simtools/applications/derive_pulse_shape_parameters.py +195 -0
- simtools/applications/plot_array_layout.py +63 -1
- simtools/applications/simulate_flasher.py +3 -2
- simtools/applications/simulate_pedestals.py +1 -1
- simtools/applications/simulate_prod.py +8 -23
- simtools/applications/simulate_prod_htcondor_generator.py +7 -0
- simtools/applications/submit_array_layouts.py +5 -3
- simtools/applications/validate_file_using_schema.py +49 -123
- simtools/configuration/commandline_parser.py +8 -6
- simtools/corsika/corsika_config.py +197 -87
- simtools/data_model/model_data_writer.py +14 -2
- simtools/data_model/schema.py +112 -5
- simtools/data_model/validate_data.py +82 -48
- simtools/db/db_model_upload.py +2 -1
- simtools/db/mongo_db.py +133 -42
- simtools/dependencies.py +5 -9
- simtools/io/eventio_handler.py +128 -0
- simtools/job_execution/htcondor_script_generator.py +0 -2
- simtools/layout/array_layout_utils.py +1 -1
- simtools/model/array_model.py +36 -5
- simtools/model/model_parameter.py +0 -1
- simtools/model/model_repository.py +18 -5
- simtools/ray_tracing/psf_analysis.py +11 -8
- simtools/ray_tracing/psf_parameter_optimisation.py +822 -679
- simtools/reporting/docs_read_parameters.py +69 -9
- simtools/runners/corsika_runner.py +12 -3
- simtools/runners/corsika_simtel_runner.py +6 -0
- simtools/runners/runner_services.py +17 -7
- simtools/runners/simtel_runner.py +12 -54
- simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +2 -0
- simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +50 -0
- simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +2 -0
- simtools/schemas/simulation_models_info.schema.yml +2 -0
- simtools/simtel/pulse_shapes.py +268 -0
- simtools/simtel/simtel_config_writer.py +82 -1
- simtools/simtel/simtel_io_event_writer.py +2 -2
- simtools/simtel/simulator_array.py +58 -12
- simtools/simtel/simulator_light_emission.py +45 -8
- simtools/simulator.py +361 -347
- simtools/testing/assertions.py +62 -6
- simtools/testing/configuration.py +1 -1
- simtools/testing/log_inspector.py +4 -1
- simtools/testing/sim_telarray_metadata.py +1 -1
- simtools/testing/validate_output.py +44 -9
- simtools/utils/names.py +2 -4
- simtools/version.py +37 -0
- simtools/visualization/legend_handlers.py +14 -4
- simtools/visualization/plot_array_layout.py +229 -33
- simtools/visualization/plot_mirrors.py +837 -0
- simtools/simtel/simtel_io_file_info.py +0 -62
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.24.0.dist-info → gammasimtools-0.25.0.dist-info}/top_level.txt +0 -0
|
@@ -11,6 +11,7 @@ to quantify the difference between measured and simulated PSF curves.
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
from collections import OrderedDict
|
|
14
|
+
from dataclasses import dataclass
|
|
14
15
|
|
|
15
16
|
import astropy.units as u
|
|
16
17
|
import numpy as np
|
|
@@ -20,6 +21,7 @@ from scipy import stats
|
|
|
20
21
|
from simtools.data_model import model_data_writer as writer
|
|
21
22
|
from simtools.ray_tracing.ray_tracing import RayTracing
|
|
22
23
|
from simtools.utils import general as gen
|
|
24
|
+
from simtools.utils import names
|
|
23
25
|
from simtools.visualization import plot_psf
|
|
24
26
|
from simtools.visualization.plot_psf import DEFAULT_FRACTION, get_psf_diameter_label
|
|
25
27
|
|
|
@@ -32,6 +34,766 @@ CUMULATIVE_PSF = "Cumulative PSF"
|
|
|
32
34
|
KS_STATISTIC_NAME = "KS statistic"
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
@dataclass
|
|
38
|
+
class GradientStepResult:
|
|
39
|
+
"""
|
|
40
|
+
Result from a gradient descent step attempt.
|
|
41
|
+
|
|
42
|
+
Attributes
|
|
43
|
+
----------
|
|
44
|
+
params : dict or None
|
|
45
|
+
Updated parameter values, None if step failed.
|
|
46
|
+
psf_diameter : float or None
|
|
47
|
+
PSF containment diameter in cm, None if step failed.
|
|
48
|
+
metric : float or None
|
|
49
|
+
Optimization metric value (RMSD or KS statistic), None if step failed.
|
|
50
|
+
p_value : float or None
|
|
51
|
+
P-value from KS test (None if using RMSD or if step failed).
|
|
52
|
+
simulated_data : numpy.ndarray or None
|
|
53
|
+
Simulated PSF data, None if step failed.
|
|
54
|
+
step_accepted : bool
|
|
55
|
+
True if the step improved the metric, False otherwise.
|
|
56
|
+
learning_rate : float
|
|
57
|
+
Final learning rate after adjustments.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
params: dict | None
|
|
61
|
+
psf_diameter: float | None
|
|
62
|
+
metric: float | None
|
|
63
|
+
p_value: float | None
|
|
64
|
+
simulated_data: np.ndarray | None
|
|
65
|
+
step_accepted: bool
|
|
66
|
+
learning_rate: float
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PSFParameterOptimizer:
|
|
70
|
+
"""
|
|
71
|
+
Gradient descent optimizer for PSF parameters.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
tel_model : TelescopeModel
|
|
76
|
+
Telescope model object containing parameter configurations.
|
|
77
|
+
site_model : SiteModel
|
|
78
|
+
Site model object with environmental conditions.
|
|
79
|
+
args_dict : dict
|
|
80
|
+
Dictionary containing simulation configuration arguments.
|
|
81
|
+
data_to_plot : dict
|
|
82
|
+
Dictionary containing measured PSF data under "measured" key.
|
|
83
|
+
radius : numpy.ndarray
|
|
84
|
+
Radius values in cm for PSF evaluation.
|
|
85
|
+
output_dir : Path
|
|
86
|
+
Directory for saving optimization results and plots.
|
|
87
|
+
|
|
88
|
+
Attributes
|
|
89
|
+
----------
|
|
90
|
+
simulation_cache : dict
|
|
91
|
+
Cache for simulation results to avoid redundant ray tracing simulations.
|
|
92
|
+
Key: frozenset of (param_name, tuple(values)) items
|
|
93
|
+
Value: (psf_diameter, metric, p_value, simulated_data)
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Learning rate adjustment constants
|
|
97
|
+
LR_REDUCTION_FACTOR = 0.7
|
|
98
|
+
LR_INCREASE_FACTOR = 2.0
|
|
99
|
+
LR_MINIMUM_THRESHOLD = 1e-6
|
|
100
|
+
LR_MAXIMUM_THRESHOLD = 0.01
|
|
101
|
+
LR_RESET_VALUE = 0.0001
|
|
102
|
+
|
|
103
|
+
def __init__(self, tel_model, site_model, args_dict, data_to_plot, radius, output_dir):
|
|
104
|
+
"""Initialize the PSF parameter optimizer."""
|
|
105
|
+
self.tel_model = tel_model
|
|
106
|
+
self.site_model = site_model
|
|
107
|
+
self.args_dict = args_dict
|
|
108
|
+
self.data_to_plot = data_to_plot
|
|
109
|
+
self.radius = radius
|
|
110
|
+
self.output_dir = output_dir
|
|
111
|
+
self.use_ks_statistic = args_dict.get("ks_statistic", False)
|
|
112
|
+
self.fraction = args_dict.get("fraction", DEFAULT_FRACTION)
|
|
113
|
+
self.simulation_cache = {}
|
|
114
|
+
self.cache_hits = 0
|
|
115
|
+
self.cache_misses = 0
|
|
116
|
+
|
|
117
|
+
def _params_to_cache_key(self, params):
|
|
118
|
+
"""Convert parameters dict to a hashable cache key."""
|
|
119
|
+
items = []
|
|
120
|
+
for key in sorted(params.keys()):
|
|
121
|
+
value = params[key]
|
|
122
|
+
if isinstance(value, list):
|
|
123
|
+
items.append((key, tuple(value)))
|
|
124
|
+
else:
|
|
125
|
+
items.append((key, value))
|
|
126
|
+
return frozenset(items)
|
|
127
|
+
|
|
128
|
+
def _reduce_learning_rate(self, current_lr):
|
|
129
|
+
"""
|
|
130
|
+
Reduce learning rate with minimum threshold and reset.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
current_lr : float
|
|
135
|
+
Current learning rate.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
float
|
|
140
|
+
Reduced learning rate, reset to LR_RESET_VALUE if below threshold.
|
|
141
|
+
"""
|
|
142
|
+
new_lr = current_lr * self.LR_REDUCTION_FACTOR
|
|
143
|
+
if new_lr < self.LR_MINIMUM_THRESHOLD:
|
|
144
|
+
return self.LR_RESET_VALUE
|
|
145
|
+
return new_lr
|
|
146
|
+
|
|
147
|
+
def _increase_learning_rate(self, current_lr):
|
|
148
|
+
"""
|
|
149
|
+
Increase learning rate.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
current_lr : float
|
|
154
|
+
Current learning rate.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
float
|
|
159
|
+
Increased learning rate, capped at LR_MAXIMUM_THRESHOLD.
|
|
160
|
+
"""
|
|
161
|
+
new_lr = current_lr * self.LR_INCREASE_FACTOR
|
|
162
|
+
return min(new_lr, self.LR_MAXIMUM_THRESHOLD)
|
|
163
|
+
|
|
164
|
+
def get_initial_parameters(self):
|
|
165
|
+
"""
|
|
166
|
+
Get current PSF parameter values from the telescope model.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
dict
|
|
171
|
+
Dictionary of current parameter values.
|
|
172
|
+
"""
|
|
173
|
+
return get_previous_values(self.tel_model)
|
|
174
|
+
|
|
175
|
+
def run_simulation(
|
|
176
|
+
self, pars, pdf_pages=None, is_best=False, use_cache=True, use_ks_statistic=None
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
Run PSF simulation for given parameters with optional caching.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
pars : dict
|
|
184
|
+
Dictionary of parameter values to test.
|
|
185
|
+
pdf_pages : PdfPages, optional
|
|
186
|
+
PDF pages object for saving plots.
|
|
187
|
+
is_best : bool, optional
|
|
188
|
+
Flag indicating if this is the best parameter set.
|
|
189
|
+
use_cache : bool, optional
|
|
190
|
+
If True, use cached results if available.
|
|
191
|
+
use_ks_statistic : bool, optional
|
|
192
|
+
If provided, override self.use_ks_statistic for this simulation.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
tuple
|
|
197
|
+
(psf_diameter, metric, p_value, simulated_data)
|
|
198
|
+
"""
|
|
199
|
+
# Determine which statistic to use
|
|
200
|
+
ks_stat = use_ks_statistic if use_ks_statistic is not None else self.use_ks_statistic
|
|
201
|
+
|
|
202
|
+
if use_cache and pdf_pages is None and not is_best:
|
|
203
|
+
cache_key = self._params_to_cache_key(pars)
|
|
204
|
+
if cache_key in self.simulation_cache:
|
|
205
|
+
self.cache_hits += 1
|
|
206
|
+
return self.simulation_cache[cache_key]
|
|
207
|
+
self.cache_misses += 1
|
|
208
|
+
|
|
209
|
+
result = run_psf_simulation(
|
|
210
|
+
self.tel_model,
|
|
211
|
+
self.site_model,
|
|
212
|
+
self.args_dict,
|
|
213
|
+
pars,
|
|
214
|
+
self.data_to_plot,
|
|
215
|
+
self.radius,
|
|
216
|
+
pdf_pages,
|
|
217
|
+
is_best,
|
|
218
|
+
ks_stat,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Cache the result if caching is enabled and not plotting
|
|
222
|
+
if use_cache and pdf_pages is None and not is_best:
|
|
223
|
+
cache_key = self._params_to_cache_key(pars)
|
|
224
|
+
self.simulation_cache[cache_key] = result
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
def calculate_gradient(self, current_params, current_metric, epsilon=0.0005):
|
|
229
|
+
"""
|
|
230
|
+
Calculate numerical gradients for all optimization parameters.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
current_params : dict
|
|
235
|
+
Dictionary of current parameter values.
|
|
236
|
+
current_metric : float
|
|
237
|
+
Current RMSD or KS statistic value.
|
|
238
|
+
epsilon : float, optional
|
|
239
|
+
Perturbation value for finite difference calculation.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
dict or None
|
|
244
|
+
Dictionary mapping parameter names to their gradient values.
|
|
245
|
+
Returns None if gradient calculation fails for any parameter.
|
|
246
|
+
"""
|
|
247
|
+
gradients = {}
|
|
248
|
+
|
|
249
|
+
for param_name, param_values in current_params.items():
|
|
250
|
+
param_gradient = self._calculate_param_gradient(
|
|
251
|
+
current_params,
|
|
252
|
+
current_metric,
|
|
253
|
+
param_name,
|
|
254
|
+
param_values,
|
|
255
|
+
epsilon,
|
|
256
|
+
)
|
|
257
|
+
if param_gradient is None:
|
|
258
|
+
return None
|
|
259
|
+
gradients[param_name] = param_gradient
|
|
260
|
+
|
|
261
|
+
return gradients
|
|
262
|
+
|
|
263
|
+
def apply_gradient_step(self, current_params, gradients, learning_rate):
|
|
264
|
+
"""
|
|
265
|
+
Apply gradient descent step to update parameters while preserving constraints.
|
|
266
|
+
|
|
267
|
+
This function applies the standard gradient descent update and preserves
|
|
268
|
+
zenith angle components (index 1) for mirror alignment parameters.
|
|
269
|
+
|
|
270
|
+
Note: Use _are_all_parameters_within_allowed_range() to validate the result before
|
|
271
|
+
accepting the step.
|
|
272
|
+
|
|
273
|
+
Parameters
|
|
274
|
+
----------
|
|
275
|
+
current_params : dict
|
|
276
|
+
Dictionary of current parameter values.
|
|
277
|
+
gradients : dict
|
|
278
|
+
Dictionary of gradient values for each parameter.
|
|
279
|
+
learning_rate : float
|
|
280
|
+
Step size for the gradient descent update.
|
|
281
|
+
|
|
282
|
+
Returns
|
|
283
|
+
-------
|
|
284
|
+
dict
|
|
285
|
+
Dictionary of updated parameter values after applying the gradient step.
|
|
286
|
+
"""
|
|
287
|
+
new_params = {}
|
|
288
|
+
for param_name, param_values in current_params.items():
|
|
289
|
+
param_gradients = gradients[param_name]
|
|
290
|
+
|
|
291
|
+
if isinstance(param_values, list):
|
|
292
|
+
updated_values = []
|
|
293
|
+
for i, (value, gradient) in enumerate(zip(param_values, param_gradients)):
|
|
294
|
+
# Apply gradient descent update
|
|
295
|
+
new_value = value - learning_rate * gradient
|
|
296
|
+
|
|
297
|
+
# Enforce constraint: preserve zenith angle (index 1) for mirror alignment
|
|
298
|
+
if (
|
|
299
|
+
param_name
|
|
300
|
+
in ["mirror_align_random_horizontal", "mirror_align_random_vertical"]
|
|
301
|
+
and i == 1
|
|
302
|
+
):
|
|
303
|
+
new_value = value # Keep original zenith angle value
|
|
304
|
+
|
|
305
|
+
updated_values.append(new_value)
|
|
306
|
+
|
|
307
|
+
new_params[param_name] = updated_values
|
|
308
|
+
else:
|
|
309
|
+
new_value = param_values - learning_rate * param_gradients
|
|
310
|
+
new_params[param_name] = new_value
|
|
311
|
+
|
|
312
|
+
return new_params
|
|
313
|
+
|
|
314
|
+
def _calculate_param_gradient(
|
|
315
|
+
self, current_params, current_metric, param_name, param_values, epsilon
|
|
316
|
+
):
|
|
317
|
+
"""
|
|
318
|
+
Calculate numerical gradient for a single parameter using finite differences.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
current_params : dict
|
|
323
|
+
Dictionary of current parameter values for all optimization parameters.
|
|
324
|
+
current_metric : float
|
|
325
|
+
Current RMSD or KS statistic value.
|
|
326
|
+
param_name : str
|
|
327
|
+
Name of the parameter for which to calculate the gradient.
|
|
328
|
+
param_values : float or list
|
|
329
|
+
Current value(s) of the parameter.
|
|
330
|
+
epsilon : float
|
|
331
|
+
Small perturbation value for finite difference calculation.
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
float or list or None
|
|
336
|
+
Gradient value(s) for the parameter.
|
|
337
|
+
Returns None if simulation fails for any component.
|
|
338
|
+
"""
|
|
339
|
+
param_gradients = []
|
|
340
|
+
values_list = param_values if isinstance(param_values, list) else [param_values]
|
|
341
|
+
|
|
342
|
+
for i, value in enumerate(values_list):
|
|
343
|
+
perturbed_params = _create_perturbed_params(
|
|
344
|
+
current_params, param_name, param_values, i, value, epsilon
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Calculate gradient for this parameter component using cached simulations
|
|
348
|
+
try:
|
|
349
|
+
_, perturbed_metric, _, _ = self.run_simulation(perturbed_params, use_cache=True)
|
|
350
|
+
gradient = (perturbed_metric - current_metric) / epsilon
|
|
351
|
+
except (ValueError, RuntimeError) as e:
|
|
352
|
+
logger.warning(f"Simulation failed for {param_name}[{i}] gradient calculation: {e}")
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
param_gradients.append(gradient)
|
|
356
|
+
|
|
357
|
+
return param_gradients[0] if not isinstance(param_values, list) else param_gradients
|
|
358
|
+
|
|
359
|
+
def perform_gradient_step_with_retries(
|
|
360
|
+
self, current_params, current_metric, learning_rate, max_retries=3
|
|
361
|
+
):
|
|
362
|
+
"""
|
|
363
|
+
Attempt gradient descent step with adaptive learning rate reduction.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
current_params : dict
|
|
368
|
+
Dictionary of current parameter values.
|
|
369
|
+
current_metric : float
|
|
370
|
+
Current optimization metric value.
|
|
371
|
+
learning_rate : float
|
|
372
|
+
Initial learning rate for the gradient descent step.
|
|
373
|
+
max_retries : int, optional
|
|
374
|
+
Maximum number of attempts with learning rate reduction.
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
GradientStepResult
|
|
379
|
+
Result object containing updated parameters and step status.
|
|
380
|
+
"""
|
|
381
|
+
current_lr = learning_rate
|
|
382
|
+
|
|
383
|
+
for attempt in range(max_retries):
|
|
384
|
+
try:
|
|
385
|
+
gradients = self.calculate_gradient(current_params, current_metric)
|
|
386
|
+
|
|
387
|
+
if gradients is None:
|
|
388
|
+
logger.warning(
|
|
389
|
+
f"Gradient calculation failed on attempt {attempt + 1}, skipping step"
|
|
390
|
+
)
|
|
391
|
+
return GradientStepResult(
|
|
392
|
+
params=None,
|
|
393
|
+
psf_diameter=None,
|
|
394
|
+
metric=None,
|
|
395
|
+
p_value=None,
|
|
396
|
+
simulated_data=None,
|
|
397
|
+
step_accepted=False,
|
|
398
|
+
learning_rate=current_lr,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
new_params = self.apply_gradient_step(current_params, gradients, current_lr)
|
|
402
|
+
|
|
403
|
+
# Validate that all parameters are within allowed ranges
|
|
404
|
+
if not _are_all_parameters_within_allowed_range(new_params):
|
|
405
|
+
logger.info(
|
|
406
|
+
f"Step rejected: parameters would go out of bounds with learning rate "
|
|
407
|
+
f"{current_lr:.6f}, reducing to {current_lr * self.LR_REDUCTION_FACTOR:.6f}"
|
|
408
|
+
)
|
|
409
|
+
current_lr = self._reduce_learning_rate(current_lr)
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
new_psf_diameter, new_metric, new_p_value, new_simulated_data = self.run_simulation(
|
|
413
|
+
new_params, use_cache=True
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if new_metric < current_metric:
|
|
417
|
+
return GradientStepResult(
|
|
418
|
+
params=new_params,
|
|
419
|
+
psf_diameter=new_psf_diameter,
|
|
420
|
+
metric=new_metric,
|
|
421
|
+
p_value=new_p_value,
|
|
422
|
+
simulated_data=new_simulated_data,
|
|
423
|
+
step_accepted=True,
|
|
424
|
+
learning_rate=current_lr,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
logger.info(
|
|
428
|
+
f"Step rejected (RMSD {current_metric:.6f} -> {new_metric:.6f}), "
|
|
429
|
+
f"reducing learning rate to {current_lr * self.LR_REDUCTION_FACTOR:.6f}"
|
|
430
|
+
)
|
|
431
|
+
current_lr = self._reduce_learning_rate(current_lr)
|
|
432
|
+
|
|
433
|
+
except (ValueError, RuntimeError, KeyError) as e:
|
|
434
|
+
logger.warning(f"Simulation failed on attempt {attempt + 1}: {e}")
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
return GradientStepResult(
|
|
438
|
+
params=None,
|
|
439
|
+
psf_diameter=None,
|
|
440
|
+
metric=None,
|
|
441
|
+
p_value=None,
|
|
442
|
+
simulated_data=None,
|
|
443
|
+
step_accepted=False,
|
|
444
|
+
learning_rate=current_lr,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
def _create_step_plot(
|
|
448
|
+
self,
|
|
449
|
+
pdf_pages,
|
|
450
|
+
current_params,
|
|
451
|
+
new_psf_diameter,
|
|
452
|
+
new_metric,
|
|
453
|
+
new_p_value,
|
|
454
|
+
new_simulated_data,
|
|
455
|
+
):
|
|
456
|
+
"""Create plot for an accepted gradient step."""
|
|
457
|
+
if (
|
|
458
|
+
pdf_pages is None
|
|
459
|
+
or not self.args_dict.get("plot_all", False)
|
|
460
|
+
or new_simulated_data is None
|
|
461
|
+
):
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
self.data_to_plot["simulated"] = new_simulated_data
|
|
465
|
+
plot_psf.create_psf_parameter_plot(
|
|
466
|
+
self.data_to_plot,
|
|
467
|
+
current_params,
|
|
468
|
+
new_psf_diameter,
|
|
469
|
+
new_metric,
|
|
470
|
+
False,
|
|
471
|
+
pdf_pages,
|
|
472
|
+
fraction=self.fraction,
|
|
473
|
+
p_value=new_p_value,
|
|
474
|
+
use_ks_statistic=self.use_ks_statistic,
|
|
475
|
+
)
|
|
476
|
+
del self.data_to_plot["simulated"]
|
|
477
|
+
|
|
478
|
+
def _create_final_plot(self, pdf_pages, best_params, best_psf_diameter):
|
|
479
|
+
"""Create final plot for best parameters."""
|
|
480
|
+
if pdf_pages is None or best_params is None:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
logger.info("Creating final plot for best parameters with both RMSD and KS statistic...")
|
|
484
|
+
_, best_ks_stat, best_p_value, best_simulated_data = self.run_simulation(
|
|
485
|
+
best_params,
|
|
486
|
+
pdf_pages=None,
|
|
487
|
+
is_best=False,
|
|
488
|
+
use_cache=False,
|
|
489
|
+
use_ks_statistic=True,
|
|
490
|
+
)
|
|
491
|
+
best_rmsd = calculate_rmsd(
|
|
492
|
+
self.data_to_plot["measured"][CUMULATIVE_PSF], best_simulated_data[CUMULATIVE_PSF]
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
self.data_to_plot["simulated"] = best_simulated_data
|
|
496
|
+
plot_psf.create_psf_parameter_plot(
|
|
497
|
+
self.data_to_plot,
|
|
498
|
+
best_params,
|
|
499
|
+
best_psf_diameter,
|
|
500
|
+
best_rmsd,
|
|
501
|
+
True,
|
|
502
|
+
pdf_pages,
|
|
503
|
+
fraction=self.fraction,
|
|
504
|
+
p_value=best_p_value,
|
|
505
|
+
use_ks_statistic=False,
|
|
506
|
+
second_metric=best_ks_stat,
|
|
507
|
+
)
|
|
508
|
+
del self.data_to_plot["simulated"]
|
|
509
|
+
pdf_pages.close()
|
|
510
|
+
logger.info("Cumulative PSF plots saved")
|
|
511
|
+
|
|
512
|
+
def run_gradient_descent(self, rmsd_threshold, learning_rate, max_iterations=200):
|
|
513
|
+
"""
|
|
514
|
+
Run gradient descent optimization to minimize PSF fitting metric.
|
|
515
|
+
|
|
516
|
+
Parameters
|
|
517
|
+
----------
|
|
518
|
+
rmsd_threshold : float
|
|
519
|
+
Convergence threshold for RMSD improvement.
|
|
520
|
+
learning_rate : float
|
|
521
|
+
Initial learning rate for gradient descent steps.
|
|
522
|
+
max_iterations : int, optional
|
|
523
|
+
Maximum number of optimization iterations.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
tuple
|
|
528
|
+
(best_params, best_psf_diameter, results)
|
|
529
|
+
"""
|
|
530
|
+
if self.data_to_plot is None or self.radius is None:
|
|
531
|
+
logger.error("No PSF measurement data provided. Cannot run optimization.")
|
|
532
|
+
return None, None, []
|
|
533
|
+
|
|
534
|
+
current_params = self.get_initial_parameters()
|
|
535
|
+
pdf_pages = plot_psf.setup_pdf_plotting(
|
|
536
|
+
self.args_dict, self.output_dir, self.tel_model.name
|
|
537
|
+
)
|
|
538
|
+
results = []
|
|
539
|
+
|
|
540
|
+
# Evaluate initial parameters
|
|
541
|
+
current_psf_diameter, current_metric, current_p_value, simulated_data = self.run_simulation(
|
|
542
|
+
current_params,
|
|
543
|
+
pdf_pages=pdf_pages if self.args_dict.get("plot_all", False) else None,
|
|
544
|
+
is_best=False,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
results.append(
|
|
548
|
+
(
|
|
549
|
+
current_params.copy(),
|
|
550
|
+
current_metric,
|
|
551
|
+
current_p_value,
|
|
552
|
+
current_psf_diameter,
|
|
553
|
+
simulated_data,
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
best_metric, best_params, best_psf_diameter = (
|
|
557
|
+
current_metric,
|
|
558
|
+
current_params.copy(),
|
|
559
|
+
current_psf_diameter,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
logger.info(
|
|
563
|
+
f"Initial RMSD: {current_metric:.6f}, PSF diameter: {current_psf_diameter:.6f} cm"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
iteration = 0
|
|
567
|
+
current_lr = learning_rate
|
|
568
|
+
|
|
569
|
+
while iteration < max_iterations:
|
|
570
|
+
if current_metric <= rmsd_threshold:
|
|
571
|
+
logger.info(
|
|
572
|
+
f"Optimization converged: RMSD {current_metric:.6f} <= "
|
|
573
|
+
f"threshold {rmsd_threshold:.6f}"
|
|
574
|
+
)
|
|
575
|
+
break
|
|
576
|
+
|
|
577
|
+
iteration += 1
|
|
578
|
+
logger.info(f"Gradient descent iteration {iteration}")
|
|
579
|
+
|
|
580
|
+
step_result = self.perform_gradient_step_with_retries(
|
|
581
|
+
current_params,
|
|
582
|
+
current_metric,
|
|
583
|
+
current_lr,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if not step_result.step_accepted or step_result.params is None:
|
|
587
|
+
current_lr = self._increase_learning_rate(current_lr)
|
|
588
|
+
logger.info(f"No step accepted, increasing learning rate to {current_lr:.6f}")
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
# Step was accepted - update state
|
|
592
|
+
current_params = step_result.params
|
|
593
|
+
current_metric = step_result.metric
|
|
594
|
+
current_psf_diameter = step_result.psf_diameter
|
|
595
|
+
current_lr = step_result.learning_rate
|
|
596
|
+
|
|
597
|
+
results.append(
|
|
598
|
+
(
|
|
599
|
+
current_params.copy(),
|
|
600
|
+
current_metric,
|
|
601
|
+
None,
|
|
602
|
+
current_psf_diameter,
|
|
603
|
+
step_result.simulated_data,
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if current_metric < best_metric:
|
|
608
|
+
best_metric, best_params, best_psf_diameter = (
|
|
609
|
+
current_metric,
|
|
610
|
+
current_params.copy(),
|
|
611
|
+
current_psf_diameter,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
self._create_step_plot(
|
|
615
|
+
pdf_pages,
|
|
616
|
+
current_params,
|
|
617
|
+
step_result.psf_diameter,
|
|
618
|
+
step_result.metric,
|
|
619
|
+
step_result.p_value,
|
|
620
|
+
step_result.simulated_data,
|
|
621
|
+
)
|
|
622
|
+
logger.info(f" Accepted step: improved to {step_result.metric:.6f}")
|
|
623
|
+
|
|
624
|
+
self._create_final_plot(pdf_pages, best_params, best_psf_diameter)
|
|
625
|
+
return best_params, best_psf_diameter, results
|
|
626
|
+
|
|
627
|
+
def analyze_monte_carlo_error(self, n_simulations=500):
|
|
628
|
+
"""
|
|
629
|
+
Analyze Monte Carlo uncertainty in PSF optimization metrics.
|
|
630
|
+
|
|
631
|
+
Parameters
|
|
632
|
+
----------
|
|
633
|
+
n_simulations : int, optional
|
|
634
|
+
Number of Monte Carlo simulations to run.
|
|
635
|
+
|
|
636
|
+
Returns
|
|
637
|
+
-------
|
|
638
|
+
tuple
|
|
639
|
+
Monte Carlo analysis results.
|
|
640
|
+
"""
|
|
641
|
+
if self.data_to_plot is None or self.radius is None:
|
|
642
|
+
logger.error("No PSF measurement data provided. Cannot analyze Monte Carlo error.")
|
|
643
|
+
return None, None, [], None, None, [], None, None, []
|
|
644
|
+
|
|
645
|
+
initial_params = self.get_initial_parameters()
|
|
646
|
+
for param_name, param_values in initial_params.items():
|
|
647
|
+
logger.info(f" {param_name}: {param_values}")
|
|
648
|
+
|
|
649
|
+
metric_values, p_values, psf_diameter_values = [], [], []
|
|
650
|
+
|
|
651
|
+
for i in range(n_simulations):
|
|
652
|
+
try:
|
|
653
|
+
psf_diameter, metric, p_value, _ = self.run_simulation(
|
|
654
|
+
initial_params,
|
|
655
|
+
use_cache=False,
|
|
656
|
+
)
|
|
657
|
+
metric_values.append(metric)
|
|
658
|
+
psf_diameter_values.append(psf_diameter)
|
|
659
|
+
p_values.append(p_value)
|
|
660
|
+
except (ValueError, RuntimeError) as e:
|
|
661
|
+
logger.warning(f"WARNING: Simulation {i + 1} failed: {e}")
|
|
662
|
+
|
|
663
|
+
if not metric_values:
|
|
664
|
+
logger.error("All Monte Carlo simulations failed.")
|
|
665
|
+
return None, None, [], None, None, [], None, None, []
|
|
666
|
+
|
|
667
|
+
mean_metric, std_metric = np.mean(metric_values), np.std(metric_values, ddof=1)
|
|
668
|
+
mean_psf_diameter, std_psf_diameter = (
|
|
669
|
+
np.mean(psf_diameter_values),
|
|
670
|
+
np.std(psf_diameter_values, ddof=1),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
if self.use_ks_statistic:
|
|
674
|
+
valid_p_values = [p for p in p_values if p is not None]
|
|
675
|
+
mean_p_value = np.mean(valid_p_values) if valid_p_values else None
|
|
676
|
+
std_p_value = np.std(valid_p_values, ddof=1) if valid_p_values else None
|
|
677
|
+
else:
|
|
678
|
+
mean_p_value = std_p_value = None
|
|
679
|
+
|
|
680
|
+
return (
|
|
681
|
+
mean_metric,
|
|
682
|
+
std_metric,
|
|
683
|
+
metric_values,
|
|
684
|
+
mean_p_value,
|
|
685
|
+
std_p_value,
|
|
686
|
+
p_values,
|
|
687
|
+
mean_psf_diameter,
|
|
688
|
+
std_psf_diameter,
|
|
689
|
+
psf_diameter_values,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _is_parameter_within_allowed_range(param_name, param_index, value):
|
|
694
|
+
"""
|
|
695
|
+
Check if a parameter value is within the allowed range defined in its schema.
|
|
696
|
+
|
|
697
|
+
Parameters
|
|
698
|
+
----------
|
|
699
|
+
param_name : str
|
|
700
|
+
Name of the parameter to check.
|
|
701
|
+
param_index : int
|
|
702
|
+
Index within the parameter array (for multi-component parameters).
|
|
703
|
+
value : float
|
|
704
|
+
The parameter value to check.
|
|
705
|
+
|
|
706
|
+
Returns
|
|
707
|
+
-------
|
|
708
|
+
bool
|
|
709
|
+
True if the parameter is within allowed range, False otherwise.
|
|
710
|
+
Returns True if no range constraints are defined for the parameter.
|
|
711
|
+
"""
|
|
712
|
+
try:
|
|
713
|
+
param_schema = names.model_parameters().get(param_name)
|
|
714
|
+
if param_schema is None:
|
|
715
|
+
return True
|
|
716
|
+
|
|
717
|
+
data = param_schema.get("data")
|
|
718
|
+
if not isinstance(data, list) or len(data) <= param_index:
|
|
719
|
+
return True
|
|
720
|
+
|
|
721
|
+
param_data = data[param_index]
|
|
722
|
+
allowed_range = param_data.get("allowed_range")
|
|
723
|
+
if not allowed_range:
|
|
724
|
+
return True
|
|
725
|
+
|
|
726
|
+
min_val = allowed_range.get("min")
|
|
727
|
+
max_val = allowed_range.get("max")
|
|
728
|
+
|
|
729
|
+
if min_val is not None and value < min_val:
|
|
730
|
+
return False
|
|
731
|
+
return not (max_val is not None and value > max_val)
|
|
732
|
+
|
|
733
|
+
except (KeyError, IndexError) as e:
|
|
734
|
+
logger.warning(f"Error reading schema for {param_name}[{param_index}]: {e}")
|
|
735
|
+
return True
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _are_all_parameters_within_allowed_range(params):
|
|
739
|
+
"""
|
|
740
|
+
Check if all parameters in the parameter dictionary are within allowed ranges.
|
|
741
|
+
|
|
742
|
+
Parameters
|
|
743
|
+
----------
|
|
744
|
+
params : dict
|
|
745
|
+
Dictionary of parameter values to validate.
|
|
746
|
+
|
|
747
|
+
Returns
|
|
748
|
+
-------
|
|
749
|
+
bool
|
|
750
|
+
True if all parameters are within allowed ranges, False otherwise.
|
|
751
|
+
"""
|
|
752
|
+
for name, values in params.items():
|
|
753
|
+
values = values if isinstance(values, list) else [values]
|
|
754
|
+
for i, v in enumerate(values):
|
|
755
|
+
if not _is_parameter_within_allowed_range(name, i, v):
|
|
756
|
+
logger.debug(f"{name}[{i}]={v:.6f} out of range")
|
|
757
|
+
return False
|
|
758
|
+
return True
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _create_perturbed_params(current_params, param_name, param_values, param_index, value, epsilon):
|
|
762
|
+
"""
|
|
763
|
+
Create parameter dictionary with one parameter perturbed by epsilon.
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
current_params : dict
|
|
768
|
+
Dictionary of current parameter values.
|
|
769
|
+
param_name : str
|
|
770
|
+
Name of the parameter to perturb.
|
|
771
|
+
param_values : float or list
|
|
772
|
+
Current parameter values.
|
|
773
|
+
param_index : int
|
|
774
|
+
Index of the parameter to perturb (for list parameters).
|
|
775
|
+
value : float
|
|
776
|
+
Current value of the specific parameter component.
|
|
777
|
+
epsilon : float
|
|
778
|
+
Perturbation amount.
|
|
779
|
+
|
|
780
|
+
Returns
|
|
781
|
+
-------
|
|
782
|
+
dict
|
|
783
|
+
New parameter dictionary with the specified parameter perturbed.
|
|
784
|
+
"""
|
|
785
|
+
perturbed_params = {
|
|
786
|
+
k: v.copy() if isinstance(v, list) else v for k, v in current_params.items()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if isinstance(param_values, list):
|
|
790
|
+
perturbed_params[param_name][param_index] = value + epsilon
|
|
791
|
+
else:
|
|
792
|
+
perturbed_params[param_name] = value + epsilon
|
|
793
|
+
|
|
794
|
+
return perturbed_params
|
|
795
|
+
|
|
796
|
+
|
|
35
797
|
def _create_log_header_and_format_value(title, tel_model, additional_info=None, value=None):
|
|
36
798
|
"""Create log header and format parameter values."""
|
|
37
799
|
if value is not None: # Format value mode
|
|
@@ -137,7 +899,7 @@ def run_psf_simulation(
|
|
|
137
899
|
Dictionary of parameter values to test in the simulation.
|
|
138
900
|
data_to_plot : dict
|
|
139
901
|
Dictionary containing measured PSF data under "measured" key.
|
|
140
|
-
radius :
|
|
902
|
+
radius : numpy.ndarray
|
|
141
903
|
Radius values in cm for PSF evaluation.
|
|
142
904
|
pdf_pages : PdfPages, optional
|
|
143
905
|
PDF pages object for saving plots (default: None).
|
|
@@ -340,7 +1102,7 @@ def export_psf_parameters(best_pars, telescope, parameter_version, output_dir):
|
|
|
340
1102
|
"""
|
|
341
1103
|
try:
|
|
342
1104
|
psf_pars_with_units = _add_units_to_psf_parameters(best_pars)
|
|
343
|
-
parameter_output_path = output_dir.
|
|
1105
|
+
parameter_output_path = output_dir.joinpath(telescope)
|
|
344
1106
|
for parameter_name, parameter_value in psf_pars_with_units.items():
|
|
345
1107
|
writer.ModelDataWriter.dump_model_parameter(
|
|
346
1108
|
parameter_name=parameter_name,
|
|
@@ -356,534 +1118,6 @@ def export_psf_parameters(best_pars, telescope, parameter_version, output_dir):
|
|
|
356
1118
|
logger.error(f"Error exporting simulation parameters: {e}")
|
|
357
1119
|
|
|
358
1120
|
|
|
359
|
-
def _calculate_param_gradient(
|
|
360
|
-
tel_model,
|
|
361
|
-
site_model,
|
|
362
|
-
args_dict,
|
|
363
|
-
current_params,
|
|
364
|
-
data_to_plot,
|
|
365
|
-
radius,
|
|
366
|
-
current_rmsd,
|
|
367
|
-
param_name,
|
|
368
|
-
param_values,
|
|
369
|
-
epsilon,
|
|
370
|
-
use_ks_statistic,
|
|
371
|
-
):
|
|
372
|
-
"""
|
|
373
|
-
Calculate numerical gradient for a single parameter using finite differences.
|
|
374
|
-
|
|
375
|
-
The gradient is calculated using forward finite differences:
|
|
376
|
-
gradient = (f(x + epsilon) - f(x)) / epsilon
|
|
377
|
-
|
|
378
|
-
Parameters
|
|
379
|
-
----------
|
|
380
|
-
tel_model : TelescopeModel
|
|
381
|
-
The telescope model object containing the current parameter configuration.
|
|
382
|
-
site_model : SiteModel
|
|
383
|
-
The site model object with environmental conditions.
|
|
384
|
-
args_dict : dict
|
|
385
|
-
Dictionary containing simulation arguments and configuration options.
|
|
386
|
-
current_params : dict
|
|
387
|
-
Dictionary of current parameter values for all optimization parameters.
|
|
388
|
-
data_to_plot : dict
|
|
389
|
-
Dictionary containing measured PSF data with "measured" key.
|
|
390
|
-
radius : array-like
|
|
391
|
-
Radius values in cm for PSF evaluation.
|
|
392
|
-
current_rmsd : float
|
|
393
|
-
Current RMSD at the current parameter configuration.
|
|
394
|
-
param_name : str
|
|
395
|
-
Name of the parameter for which to calculate the gradient.
|
|
396
|
-
param_values : float or list
|
|
397
|
-
Current value(s) of the parameter. Can be a single value or list of values.
|
|
398
|
-
epsilon : float
|
|
399
|
-
Small perturbation value for finite difference calculation.
|
|
400
|
-
use_ks_statistic : bool
|
|
401
|
-
If True, calculate gradient with respect to KS statistic; if False, use RMSD.
|
|
402
|
-
|
|
403
|
-
Returns
|
|
404
|
-
-------
|
|
405
|
-
float or list
|
|
406
|
-
Gradient value(s) for the parameter. Returns a single float if param_values
|
|
407
|
-
is a single value, or a list of gradients if param_values is a list.
|
|
408
|
-
|
|
409
|
-
If a simulation fails during gradient calculation, a gradient of 0.0 is assigned
|
|
410
|
-
for that component to ensure the optimization can continue.
|
|
411
|
-
"""
|
|
412
|
-
param_gradients = []
|
|
413
|
-
values_list = param_values if isinstance(param_values, list) else [param_values]
|
|
414
|
-
|
|
415
|
-
for i, value in enumerate(values_list):
|
|
416
|
-
perturbed_params = {
|
|
417
|
-
k: v.copy() if isinstance(v, list) else v for k, v in current_params.items()
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if isinstance(param_values, list):
|
|
421
|
-
perturbed_params[param_name][i] = value + epsilon
|
|
422
|
-
else:
|
|
423
|
-
perturbed_params[param_name] = value + epsilon
|
|
424
|
-
|
|
425
|
-
try:
|
|
426
|
-
_, perturbed_rmsd, _, _ = run_psf_simulation(
|
|
427
|
-
tel_model,
|
|
428
|
-
site_model,
|
|
429
|
-
args_dict,
|
|
430
|
-
perturbed_params,
|
|
431
|
-
data_to_plot,
|
|
432
|
-
radius,
|
|
433
|
-
pdf_pages=None,
|
|
434
|
-
is_best=False,
|
|
435
|
-
use_ks_statistic=use_ks_statistic,
|
|
436
|
-
)
|
|
437
|
-
param_gradients.append((perturbed_rmsd - current_rmsd) / epsilon)
|
|
438
|
-
except (ValueError, RuntimeError):
|
|
439
|
-
param_gradients.append(0.0)
|
|
440
|
-
|
|
441
|
-
return param_gradients[0] if not isinstance(param_values, list) else param_gradients
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
def calculate_gradient(
|
|
445
|
-
tel_model,
|
|
446
|
-
site_model,
|
|
447
|
-
args_dict,
|
|
448
|
-
current_params,
|
|
449
|
-
data_to_plot,
|
|
450
|
-
radius,
|
|
451
|
-
current_rmsd,
|
|
452
|
-
epsilon=0.0005,
|
|
453
|
-
use_ks_statistic=False,
|
|
454
|
-
):
|
|
455
|
-
"""
|
|
456
|
-
Calculate numerical gradients for all optimization parameters.
|
|
457
|
-
|
|
458
|
-
Parameters
|
|
459
|
-
----------
|
|
460
|
-
tel_model : TelescopeModel
|
|
461
|
-
Telescope model object for simulations.
|
|
462
|
-
site_model : SiteModel
|
|
463
|
-
Site model object with environmental conditions.
|
|
464
|
-
args_dict : dict
|
|
465
|
-
Dictionary containing simulation configuration arguments.
|
|
466
|
-
current_params : dict
|
|
467
|
-
Dictionary of current parameter values for all optimization parameters.
|
|
468
|
-
data_to_plot : dict
|
|
469
|
-
Dictionary containing measured PSF data.
|
|
470
|
-
radius : array-like
|
|
471
|
-
Radius values in cm for PSF evaluation.
|
|
472
|
-
current_rmsd : float
|
|
473
|
-
Current RMSD or KS statistic value.
|
|
474
|
-
epsilon : float, optional
|
|
475
|
-
Perturbation value for finite difference calculation (default: 0.0005).
|
|
476
|
-
use_ks_statistic : bool, optional
|
|
477
|
-
If True, calculate gradients for KS statistic; if False, use RMSD (default: False).
|
|
478
|
-
|
|
479
|
-
Returns
|
|
480
|
-
-------
|
|
481
|
-
dict
|
|
482
|
-
Dictionary mapping parameter names to their gradient values.
|
|
483
|
-
For parameters with multiple components, gradients are returned as lists.
|
|
484
|
-
"""
|
|
485
|
-
gradients = {}
|
|
486
|
-
|
|
487
|
-
for param_name, param_values in current_params.items():
|
|
488
|
-
gradients[param_name] = _calculate_param_gradient(
|
|
489
|
-
tel_model,
|
|
490
|
-
site_model,
|
|
491
|
-
args_dict,
|
|
492
|
-
current_params,
|
|
493
|
-
data_to_plot,
|
|
494
|
-
radius,
|
|
495
|
-
current_rmsd,
|
|
496
|
-
param_name,
|
|
497
|
-
param_values,
|
|
498
|
-
epsilon,
|
|
499
|
-
use_ks_statistic,
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
return gradients
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def apply_gradient_step(current_params, gradients, learning_rate):
|
|
506
|
-
"""
|
|
507
|
-
Apply gradient descent step to update parameters.
|
|
508
|
-
|
|
509
|
-
Parameters
|
|
510
|
-
----------
|
|
511
|
-
current_params : dict
|
|
512
|
-
Dictionary of current parameter values.
|
|
513
|
-
gradients : dict
|
|
514
|
-
Dictionary of gradient values for each parameter.
|
|
515
|
-
learning_rate : float
|
|
516
|
-
Step size for the gradient descent update.
|
|
517
|
-
|
|
518
|
-
Returns
|
|
519
|
-
-------
|
|
520
|
-
dict
|
|
521
|
-
Dictionary of updated parameter values after applying the gradient step.
|
|
522
|
-
"""
|
|
523
|
-
new_params = {}
|
|
524
|
-
for param_name, param_values in current_params.items():
|
|
525
|
-
param_gradients = gradients[param_name]
|
|
526
|
-
|
|
527
|
-
if isinstance(param_values, list):
|
|
528
|
-
new_params[param_name] = [
|
|
529
|
-
value - learning_rate * gradient
|
|
530
|
-
for value, gradient in zip(param_values, param_gradients)
|
|
531
|
-
]
|
|
532
|
-
else:
|
|
533
|
-
new_params[param_name] = param_values - learning_rate * param_gradients
|
|
534
|
-
|
|
535
|
-
return new_params
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def _perform_gradient_step_with_retries(
|
|
539
|
-
tel_model,
|
|
540
|
-
site_model,
|
|
541
|
-
args_dict,
|
|
542
|
-
current_params,
|
|
543
|
-
current_metric,
|
|
544
|
-
data_to_plot,
|
|
545
|
-
radius,
|
|
546
|
-
learning_rate,
|
|
547
|
-
max_retries=3,
|
|
548
|
-
):
|
|
549
|
-
"""
|
|
550
|
-
Attempt gradient descent step with adaptive learning rate reduction on rejection.
|
|
551
|
-
|
|
552
|
-
The learning rate reduction strategy follows these rules:
|
|
553
|
-
- If step is rejected: learning_rate *= 0.7
|
|
554
|
-
- If attempt number < number of max retries then try again
|
|
555
|
-
- If learning_rate drops below 1e-5: reset to 0.001
|
|
556
|
-
- If all retries fail: returns None values with step_accepted=False
|
|
557
|
-
|
|
558
|
-
This adaptive approach helps navigate local minima and ensures robust convergence
|
|
559
|
-
by automatically adjusting the step size based on optimization progress.
|
|
560
|
-
|
|
561
|
-
Parameters
|
|
562
|
-
----------
|
|
563
|
-
tel_model : TelescopeModel
|
|
564
|
-
Telescope model object containing the current parameter configuration.
|
|
565
|
-
site_model : SiteModel
|
|
566
|
-
Site model object with environmental conditions for ray tracing simulations.
|
|
567
|
-
args_dict : dict
|
|
568
|
-
Dictionary containing simulation configuration arguments and settings.
|
|
569
|
-
current_params : dict
|
|
570
|
-
Dictionary of current parameter values for all optimization parameters.
|
|
571
|
-
current_metric : float
|
|
572
|
-
Current optimization metric value (RMSD or KS statistic) to improve upon.
|
|
573
|
-
data_to_plot : dict
|
|
574
|
-
Dictionary containing measured PSF data under "measured" key for comparison.
|
|
575
|
-
radius : array-like
|
|
576
|
-
Radius values in cm for PSF evaluation and comparison.
|
|
577
|
-
learning_rate : float
|
|
578
|
-
Initial learning rate for the gradient descent step.
|
|
579
|
-
max_retries : int, optional
|
|
580
|
-
Maximum number of attempts with learning rate reduction (default: 3).
|
|
581
|
-
|
|
582
|
-
Returns
|
|
583
|
-
-------
|
|
584
|
-
tuple of (dict, float, float, float or None, array, bool, float)
|
|
585
|
-
- new_params: Updated parameter dictionary if step accepted, None if rejected
|
|
586
|
-
- new_psf_diameter: PSF containment diameter in cm for new parameters, None if step rejected
|
|
587
|
-
- new_metric: New optimization metric value, None if step rejected
|
|
588
|
-
- new_p_value: p-value from KS test if applicable, None otherwise
|
|
589
|
-
- new_simulated_data: Simulated PSF data array, None if step rejected
|
|
590
|
-
- step_accepted: Boolean indicating if any step was accepted
|
|
591
|
-
- final_learning_rate: Learning rate after potential reductions
|
|
592
|
-
|
|
593
|
-
"""
|
|
594
|
-
current_lr = learning_rate
|
|
595
|
-
|
|
596
|
-
for attempt in range(max_retries):
|
|
597
|
-
try:
|
|
598
|
-
gradients = calculate_gradient(
|
|
599
|
-
tel_model,
|
|
600
|
-
site_model,
|
|
601
|
-
args_dict,
|
|
602
|
-
current_params,
|
|
603
|
-
data_to_plot,
|
|
604
|
-
radius,
|
|
605
|
-
current_metric,
|
|
606
|
-
use_ks_statistic=False,
|
|
607
|
-
)
|
|
608
|
-
new_params = apply_gradient_step(current_params, gradients, current_lr)
|
|
609
|
-
|
|
610
|
-
new_psf_diameter, new_metric, new_p_value, new_simulated_data = run_psf_simulation(
|
|
611
|
-
tel_model,
|
|
612
|
-
site_model,
|
|
613
|
-
args_dict,
|
|
614
|
-
new_params,
|
|
615
|
-
data_to_plot,
|
|
616
|
-
radius,
|
|
617
|
-
pdf_pages=None,
|
|
618
|
-
is_best=False,
|
|
619
|
-
use_ks_statistic=False,
|
|
620
|
-
)
|
|
621
|
-
|
|
622
|
-
if new_metric < current_metric:
|
|
623
|
-
return (
|
|
624
|
-
new_params,
|
|
625
|
-
new_psf_diameter,
|
|
626
|
-
new_metric,
|
|
627
|
-
new_p_value,
|
|
628
|
-
new_simulated_data,
|
|
629
|
-
True,
|
|
630
|
-
current_lr,
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
logger.info(
|
|
634
|
-
f"Step rejected (RMSD {current_metric:.6f} -> {new_metric:.6f}), "
|
|
635
|
-
f"reducing learning rate {current_lr:.6f} -> {current_lr * 0.7:.6f}"
|
|
636
|
-
)
|
|
637
|
-
current_lr *= 0.7
|
|
638
|
-
|
|
639
|
-
if current_lr < 1e-5:
|
|
640
|
-
current_lr = 0.001
|
|
641
|
-
|
|
642
|
-
except (ValueError, RuntimeError, KeyError) as e:
|
|
643
|
-
logger.warning(f"Simulation failed on attempt {attempt + 1}: {e}")
|
|
644
|
-
continue
|
|
645
|
-
|
|
646
|
-
return None, None, None, None, None, False, current_lr
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
def _create_step_plot(
|
|
650
|
-
pdf_pages,
|
|
651
|
-
args_dict,
|
|
652
|
-
data_to_plot,
|
|
653
|
-
current_params,
|
|
654
|
-
new_psf_diameter,
|
|
655
|
-
new_metric,
|
|
656
|
-
new_p_value,
|
|
657
|
-
new_simulated_data,
|
|
658
|
-
):
|
|
659
|
-
"""Create plot for an accepted gradient step."""
|
|
660
|
-
if pdf_pages is None or not args_dict.get("plot_all", False) or new_simulated_data is None:
|
|
661
|
-
return
|
|
662
|
-
|
|
663
|
-
data_to_plot["simulated"] = new_simulated_data
|
|
664
|
-
plot_psf.create_psf_parameter_plot(
|
|
665
|
-
data_to_plot,
|
|
666
|
-
current_params,
|
|
667
|
-
new_psf_diameter,
|
|
668
|
-
new_metric,
|
|
669
|
-
False,
|
|
670
|
-
pdf_pages,
|
|
671
|
-
fraction=args_dict.get("fraction", DEFAULT_FRACTION),
|
|
672
|
-
p_value=new_p_value,
|
|
673
|
-
use_ks_statistic=False,
|
|
674
|
-
)
|
|
675
|
-
del data_to_plot["simulated"]
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
def _create_final_plot(
|
|
679
|
-
pdf_pages,
|
|
680
|
-
tel_model,
|
|
681
|
-
site_model,
|
|
682
|
-
args_dict,
|
|
683
|
-
best_params,
|
|
684
|
-
data_to_plot,
|
|
685
|
-
radius,
|
|
686
|
-
best_psf_diameter,
|
|
687
|
-
):
|
|
688
|
-
"""Create final plot for best parameters."""
|
|
689
|
-
if pdf_pages is None or best_params is None:
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
logger.info("Creating final plot for best parameters with both RMSD and KS statistic...")
|
|
693
|
-
_, best_ks_stat, best_p_value, best_simulated_data = run_psf_simulation(
|
|
694
|
-
tel_model,
|
|
695
|
-
site_model,
|
|
696
|
-
args_dict,
|
|
697
|
-
best_params,
|
|
698
|
-
data_to_plot,
|
|
699
|
-
radius,
|
|
700
|
-
pdf_pages=None,
|
|
701
|
-
is_best=False,
|
|
702
|
-
use_ks_statistic=True,
|
|
703
|
-
)
|
|
704
|
-
best_rmsd = calculate_rmsd(
|
|
705
|
-
data_to_plot["measured"][CUMULATIVE_PSF], best_simulated_data[CUMULATIVE_PSF]
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
data_to_plot["simulated"] = best_simulated_data
|
|
709
|
-
plot_psf.create_psf_parameter_plot(
|
|
710
|
-
data_to_plot,
|
|
711
|
-
best_params,
|
|
712
|
-
best_psf_diameter,
|
|
713
|
-
best_rmsd,
|
|
714
|
-
True,
|
|
715
|
-
pdf_pages,
|
|
716
|
-
fraction=args_dict.get("fraction", DEFAULT_FRACTION),
|
|
717
|
-
p_value=best_p_value,
|
|
718
|
-
use_ks_statistic=False,
|
|
719
|
-
second_metric=best_ks_stat,
|
|
720
|
-
)
|
|
721
|
-
del data_to_plot["simulated"]
|
|
722
|
-
pdf_pages.close()
|
|
723
|
-
logger.info("Cumulative PSF plots saved")
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
def run_gradient_descent_optimization(
|
|
727
|
-
tel_model,
|
|
728
|
-
site_model,
|
|
729
|
-
args_dict,
|
|
730
|
-
data_to_plot,
|
|
731
|
-
radius,
|
|
732
|
-
rmsd_threshold,
|
|
733
|
-
learning_rate,
|
|
734
|
-
output_dir,
|
|
735
|
-
):
|
|
736
|
-
"""
|
|
737
|
-
Run gradient descent optimization to minimize PSF fitting metric.
|
|
738
|
-
|
|
739
|
-
Parameters
|
|
740
|
-
----------
|
|
741
|
-
tel_model : TelescopeModel
|
|
742
|
-
Telescope model object to be optimized.
|
|
743
|
-
site_model : SiteModel
|
|
744
|
-
Site model object with environmental conditions.
|
|
745
|
-
args_dict : dict
|
|
746
|
-
Dictionary containing simulation configuration arguments.
|
|
747
|
-
data_to_plot : dict
|
|
748
|
-
Dictionary containing measured PSF data under "measured" key.
|
|
749
|
-
radius : array-like
|
|
750
|
-
Radius values in cm for PSF evaluation.
|
|
751
|
-
rmsd_threshold : float
|
|
752
|
-
Convergence threshold for RMSD improvement.
|
|
753
|
-
learning_rate : float
|
|
754
|
-
Initial learning rate for gradient descent steps.
|
|
755
|
-
output_dir : Path
|
|
756
|
-
Directory for saving optimization plots and results.
|
|
757
|
-
|
|
758
|
-
Returns
|
|
759
|
-
-------
|
|
760
|
-
tuple of (dict, float, list)
|
|
761
|
-
- best_params: Dictionary of optimized parameter values
|
|
762
|
-
- best_psf_diameter: PSF containment diameter in cm for the best parameters
|
|
763
|
-
- results: List of (params, metric, p_value, psf_diameter, simulated_data)
|
|
764
|
-
for each iteration
|
|
765
|
-
|
|
766
|
-
Returns None values if optimization fails or no measurement data is provided.
|
|
767
|
-
"""
|
|
768
|
-
if data_to_plot is None or radius is None:
|
|
769
|
-
logger.error("No PSF measurement data provided. Cannot run optimization.")
|
|
770
|
-
return None, None, []
|
|
771
|
-
|
|
772
|
-
current_params = get_previous_values(tel_model)
|
|
773
|
-
pdf_pages = plot_psf.setup_pdf_plotting(args_dict, output_dir, tel_model.name)
|
|
774
|
-
results = []
|
|
775
|
-
|
|
776
|
-
# Evaluate initial parameters
|
|
777
|
-
current_psf_diameter, current_metric, current_p_value, simulated_data = run_psf_simulation(
|
|
778
|
-
tel_model,
|
|
779
|
-
site_model,
|
|
780
|
-
args_dict,
|
|
781
|
-
current_params,
|
|
782
|
-
data_to_plot,
|
|
783
|
-
radius,
|
|
784
|
-
pdf_pages=pdf_pages if args_dict.get("plot_all", False) else None,
|
|
785
|
-
is_best=False,
|
|
786
|
-
use_ks_statistic=False,
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
results.append(
|
|
790
|
-
(
|
|
791
|
-
current_params.copy(),
|
|
792
|
-
current_metric,
|
|
793
|
-
current_p_value,
|
|
794
|
-
current_psf_diameter,
|
|
795
|
-
simulated_data,
|
|
796
|
-
)
|
|
797
|
-
)
|
|
798
|
-
best_metric, best_params, best_psf_diameter = (
|
|
799
|
-
current_metric,
|
|
800
|
-
current_params.copy(),
|
|
801
|
-
current_psf_diameter,
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
logger.info(f"Initial RMSD: {current_metric:.6f}, PSF diameter: {current_psf_diameter:.6f} cm")
|
|
805
|
-
|
|
806
|
-
iteration = 0
|
|
807
|
-
max_total_iterations = 100
|
|
808
|
-
|
|
809
|
-
while iteration < max_total_iterations:
|
|
810
|
-
if current_metric <= rmsd_threshold:
|
|
811
|
-
logger.info(
|
|
812
|
-
f"Optimization converged: RMSD {current_metric:.6f} <= "
|
|
813
|
-
f"threshold {rmsd_threshold:.6f}"
|
|
814
|
-
)
|
|
815
|
-
break
|
|
816
|
-
|
|
817
|
-
iteration += 1
|
|
818
|
-
logger.info(f"Gradient descent iteration {iteration}")
|
|
819
|
-
|
|
820
|
-
step_result = _perform_gradient_step_with_retries(
|
|
821
|
-
tel_model,
|
|
822
|
-
site_model,
|
|
823
|
-
args_dict,
|
|
824
|
-
current_params,
|
|
825
|
-
current_metric,
|
|
826
|
-
data_to_plot,
|
|
827
|
-
radius,
|
|
828
|
-
learning_rate,
|
|
829
|
-
)
|
|
830
|
-
(
|
|
831
|
-
new_params,
|
|
832
|
-
new_psf_diameter,
|
|
833
|
-
new_metric,
|
|
834
|
-
new_p_value,
|
|
835
|
-
new_simulated_data,
|
|
836
|
-
step_accepted,
|
|
837
|
-
learning_rate,
|
|
838
|
-
) = step_result
|
|
839
|
-
|
|
840
|
-
if not step_accepted or new_params is None:
|
|
841
|
-
learning_rate *= 2.0
|
|
842
|
-
logger.info(f"No step accepted, increasing learning rate to {learning_rate:.6f}")
|
|
843
|
-
continue
|
|
844
|
-
|
|
845
|
-
# Step was accepted - update state
|
|
846
|
-
current_params, current_metric, current_psf_diameter = (
|
|
847
|
-
new_params,
|
|
848
|
-
new_metric,
|
|
849
|
-
new_psf_diameter,
|
|
850
|
-
)
|
|
851
|
-
results.append(
|
|
852
|
-
(current_params.copy(), current_metric, None, current_psf_diameter, new_simulated_data)
|
|
853
|
-
)
|
|
854
|
-
|
|
855
|
-
if current_metric < best_metric:
|
|
856
|
-
best_metric, best_params, best_psf_diameter = (
|
|
857
|
-
current_metric,
|
|
858
|
-
current_params.copy(),
|
|
859
|
-
current_psf_diameter,
|
|
860
|
-
)
|
|
861
|
-
|
|
862
|
-
_create_step_plot(
|
|
863
|
-
pdf_pages,
|
|
864
|
-
args_dict,
|
|
865
|
-
data_to_plot,
|
|
866
|
-
current_params,
|
|
867
|
-
new_psf_diameter,
|
|
868
|
-
new_metric,
|
|
869
|
-
new_p_value,
|
|
870
|
-
new_simulated_data,
|
|
871
|
-
)
|
|
872
|
-
logger.info(f" Accepted step: improved to {new_metric:.6f}")
|
|
873
|
-
|
|
874
|
-
_create_final_plot(
|
|
875
|
-
pdf_pages,
|
|
876
|
-
tel_model,
|
|
877
|
-
site_model,
|
|
878
|
-
args_dict,
|
|
879
|
-
best_params,
|
|
880
|
-
data_to_plot,
|
|
881
|
-
radius,
|
|
882
|
-
best_psf_diameter,
|
|
883
|
-
)
|
|
884
|
-
return best_params, best_psf_diameter, results
|
|
885
|
-
|
|
886
|
-
|
|
887
1121
|
def _write_log_interpretation(f, use_ks_statistic):
|
|
888
1122
|
"""Write interpretation section for the log file."""
|
|
889
1123
|
if use_ks_statistic:
|
|
@@ -1027,103 +1261,6 @@ def write_gradient_descent_log(
|
|
|
1027
1261
|
return param_file
|
|
1028
1262
|
|
|
1029
1263
|
|
|
1030
|
-
def analyze_monte_carlo_error(
|
|
1031
|
-
tel_model, site_model, args_dict, data_to_plot, radius, n_simulations=500
|
|
1032
|
-
):
|
|
1033
|
-
"""
|
|
1034
|
-
Analyze Monte Carlo uncertainty in PSF optimization metrics.
|
|
1035
|
-
|
|
1036
|
-
Runs multiple simulations with the same parameters to quantify the
|
|
1037
|
-
statistical uncertainty in the optimization metric due to Monte Carlo
|
|
1038
|
-
noise in the ray tracing simulations. Returns None values if no
|
|
1039
|
-
measurement data is provided or all simulations fail.
|
|
1040
|
-
|
|
1041
|
-
Parameters
|
|
1042
|
-
----------
|
|
1043
|
-
tel_model : TelescopeModel
|
|
1044
|
-
Telescope model object with current parameter configuration.
|
|
1045
|
-
site_model : SiteModel
|
|
1046
|
-
Site model object with environmental conditions.
|
|
1047
|
-
args_dict : dict
|
|
1048
|
-
Dictionary containing simulation configuration arguments.
|
|
1049
|
-
data_to_plot : dict
|
|
1050
|
-
Dictionary containing measured PSF data under "measured" key.
|
|
1051
|
-
radius : array-like
|
|
1052
|
-
Radius values in cm for PSF evaluation.
|
|
1053
|
-
n_simulations : int, optional
|
|
1054
|
-
Number of Monte Carlo simulations to run (default: 500).
|
|
1055
|
-
|
|
1056
|
-
Returns
|
|
1057
|
-
-------
|
|
1058
|
-
tuple of (float, float, list, float, float, list, float, float, list)
|
|
1059
|
-
- mean_metric: Mean RMSD or KS statistic value
|
|
1060
|
-
- std_metric: Standard deviation of metric values
|
|
1061
|
-
- metric_values: List of all metric values from simulations
|
|
1062
|
-
- mean_p_value: Mean p-value (None if using RMSD)
|
|
1063
|
-
- std_p_value: Standard deviation of p-values (None if using RMSD)
|
|
1064
|
-
- p_values: List of all p-values from simulations
|
|
1065
|
-
- mean_psf_diameter: Mean PSF containment diameter in cm
|
|
1066
|
-
- std_psf_diameter: Standard deviation of PSF diameter values
|
|
1067
|
-
- psf_diameter_values: List of all PSF diameter values from simulations
|
|
1068
|
-
"""
|
|
1069
|
-
if data_to_plot is None or radius is None:
|
|
1070
|
-
logger.error("No PSF measurement data provided. Cannot analyze Monte Carlo error.")
|
|
1071
|
-
return None, None, []
|
|
1072
|
-
|
|
1073
|
-
initial_params = get_previous_values(tel_model)
|
|
1074
|
-
for param_name, param_values in initial_params.items():
|
|
1075
|
-
logger.info(f" {param_name}: {param_values}")
|
|
1076
|
-
|
|
1077
|
-
use_ks_statistic = args_dict.get("ks_statistic", False)
|
|
1078
|
-
metric_values, p_values, psf_diameter_values = [], [], []
|
|
1079
|
-
|
|
1080
|
-
for i in range(n_simulations):
|
|
1081
|
-
try:
|
|
1082
|
-
psf_diameter, metric, p_value, _ = run_psf_simulation(
|
|
1083
|
-
tel_model,
|
|
1084
|
-
site_model,
|
|
1085
|
-
args_dict,
|
|
1086
|
-
initial_params,
|
|
1087
|
-
data_to_plot,
|
|
1088
|
-
radius,
|
|
1089
|
-
use_ks_statistic=use_ks_statistic,
|
|
1090
|
-
)
|
|
1091
|
-
metric_values.append(metric)
|
|
1092
|
-
psf_diameter_values.append(psf_diameter)
|
|
1093
|
-
p_values.append(p_value)
|
|
1094
|
-
except (ValueError, RuntimeError) as e:
|
|
1095
|
-
logger.warning(f"WARNING: Simulation {i + 1} failed: {e}")
|
|
1096
|
-
|
|
1097
|
-
if not metric_values:
|
|
1098
|
-
logger.error("All Monte Carlo simulations failed.")
|
|
1099
|
-
return None, None, [], None, None, []
|
|
1100
|
-
|
|
1101
|
-
mean_metric, std_metric = np.mean(metric_values), np.std(metric_values, ddof=1)
|
|
1102
|
-
mean_psf_diameter, std_psf_diameter = (
|
|
1103
|
-
np.mean(psf_diameter_values),
|
|
1104
|
-
np.std(psf_diameter_values, ddof=1),
|
|
1105
|
-
)
|
|
1106
|
-
|
|
1107
|
-
if use_ks_statistic:
|
|
1108
|
-
valid_p_values = [p for p in p_values if p is not None]
|
|
1109
|
-
mean_p_value = np.mean(valid_p_values) if valid_p_values else None
|
|
1110
|
-
std_p_value = np.std(valid_p_values, ddof=1) if valid_p_values else None
|
|
1111
|
-
else:
|
|
1112
|
-
mean_p_value = std_p_value = None
|
|
1113
|
-
|
|
1114
|
-
return (
|
|
1115
|
-
mean_metric,
|
|
1116
|
-
std_metric,
|
|
1117
|
-
metric_values,
|
|
1118
|
-
mean_p_value,
|
|
1119
|
-
std_p_value,
|
|
1120
|
-
p_values,
|
|
1121
|
-
mean_psf_diameter,
|
|
1122
|
-
std_psf_diameter,
|
|
1123
|
-
psf_diameter_values,
|
|
1124
|
-
)
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
1264
|
def write_monte_carlo_analysis(
|
|
1128
1265
|
mc_results, output_dir, tel_model, use_ks_statistic=False, fraction=DEFAULT_FRACTION
|
|
1129
1266
|
):
|
|
@@ -1222,12 +1359,7 @@ def write_monte_carlo_analysis(
|
|
|
1222
1359
|
zip(metric_values, p_values, psf_diameter_values)
|
|
1223
1360
|
):
|
|
1224
1361
|
if use_ks_statistic and p_value is not None:
|
|
1225
|
-
|
|
1226
|
-
significance = "GOOD"
|
|
1227
|
-
elif p_value > 0.01:
|
|
1228
|
-
significance = "FAIR"
|
|
1229
|
-
else:
|
|
1230
|
-
significance = "POOR"
|
|
1362
|
+
significance = plot_psf.get_significance_label(p_value)
|
|
1231
1363
|
f.write(
|
|
1232
1364
|
f"Simulation {i + 1:2d}: {metric_name}={metric_val:.6f}, "
|
|
1233
1365
|
f"p_value={p_value:.6f} ({significance}), {psf_label}={psf_diameter:.6f} cm\n"
|
|
@@ -1241,63 +1373,71 @@ def write_monte_carlo_analysis(
|
|
|
1241
1373
|
return mc_file
|
|
1242
1374
|
|
|
1243
1375
|
|
|
1244
|
-
def
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
)
|
|
1265
|
-
return True
|
|
1376
|
+
def cleanup_intermediate_files(output_dir):
|
|
1377
|
+
"""
|
|
1378
|
+
Remove intermediate log and list files from the output directory.
|
|
1379
|
+
|
|
1380
|
+
Parameters
|
|
1381
|
+
----------
|
|
1382
|
+
output_dir : Path
|
|
1383
|
+
Directory containing output files to clean up.
|
|
1384
|
+
"""
|
|
1385
|
+
patterns = ["*.log", "*.lis*"]
|
|
1386
|
+
files_removed = 0
|
|
1387
|
+
|
|
1388
|
+
for pattern in patterns:
|
|
1389
|
+
for file_path in output_dir.glob(pattern):
|
|
1390
|
+
file_path.unlink()
|
|
1391
|
+
files_removed += 1
|
|
1392
|
+
logger.debug(f"Removed: {file_path.name}")
|
|
1393
|
+
|
|
1394
|
+
if files_removed > 0:
|
|
1395
|
+
logger.info(f"Cleanup: removed {files_removed} intermediate files")
|
|
1266
1396
|
|
|
1267
1397
|
|
|
1268
1398
|
def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
1269
|
-
"""
|
|
1399
|
+
"""
|
|
1400
|
+
Run the complete PSF parameter optimization workflow using gradient descent.
|
|
1401
|
+
|
|
1402
|
+
This function creates a PSFParameterOptimizer instance and orchestrates
|
|
1403
|
+
the optimization process.
|
|
1404
|
+
"""
|
|
1270
1405
|
data_to_plot, radius = load_and_process_data(args_dict)
|
|
1271
|
-
use_ks_statistic = args_dict.get("ks_statistic", False)
|
|
1272
1406
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1407
|
+
# Create optimizer instance to encapsulate state and methods
|
|
1408
|
+
optimizer = PSFParameterOptimizer(
|
|
1409
|
+
tel_model, site_model, args_dict, data_to_plot, radius, output_dir
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
# Handle Monte Carlo analysis if requested
|
|
1413
|
+
if args_dict.get("monte_carlo_analysis", False):
|
|
1414
|
+
mc_results = optimizer.analyze_monte_carlo_error()
|
|
1415
|
+
if mc_results[0] is not None:
|
|
1416
|
+
mc_file = write_monte_carlo_analysis(
|
|
1417
|
+
mc_results,
|
|
1418
|
+
output_dir,
|
|
1419
|
+
tel_model,
|
|
1420
|
+
optimizer.use_ks_statistic,
|
|
1421
|
+
optimizer.fraction,
|
|
1422
|
+
)
|
|
1423
|
+
logger.info(f"Monte Carlo analysis results written to {mc_file}")
|
|
1424
|
+
mc_plot_file = output_dir.joinpath(f"monte_carlo_uncertainty_{tel_model.name}.pdf")
|
|
1425
|
+
plot_psf.create_monte_carlo_uncertainty_plot(
|
|
1426
|
+
mc_results, mc_plot_file, optimizer.fraction, optimizer.use_ks_statistic
|
|
1427
|
+
)
|
|
1276
1428
|
return
|
|
1277
1429
|
|
|
1278
1430
|
# Run gradient descent optimization
|
|
1279
1431
|
threshold = args_dict.get("rmsd_threshold")
|
|
1280
1432
|
learning_rate = args_dict.get("learning_rate")
|
|
1281
1433
|
|
|
1282
|
-
best_pars, best_psf_diameter, gd_results =
|
|
1283
|
-
|
|
1284
|
-
site_model,
|
|
1285
|
-
args_dict,
|
|
1286
|
-
data_to_plot,
|
|
1287
|
-
radius,
|
|
1288
|
-
rmsd_threshold=threshold,
|
|
1289
|
-
learning_rate=learning_rate,
|
|
1290
|
-
output_dir=output_dir,
|
|
1434
|
+
best_pars, best_psf_diameter, gd_results = optimizer.run_gradient_descent(
|
|
1435
|
+
threshold, learning_rate
|
|
1291
1436
|
)
|
|
1292
1437
|
|
|
1293
1438
|
# Check if optimization was successful
|
|
1294
1439
|
if not gd_results or best_pars is None:
|
|
1295
|
-
logger.error("Gradient descent optimization failed
|
|
1296
|
-
if radius is None:
|
|
1297
|
-
logger.error(
|
|
1298
|
-
"Possible cause: No PSF measurement data provided. "
|
|
1299
|
-
"Use --data argument to provide PSF data."
|
|
1300
|
-
)
|
|
1440
|
+
logger.error("Gradient descent optimization failed to produce results.")
|
|
1301
1441
|
return
|
|
1302
1442
|
|
|
1303
1443
|
plot_psf.create_optimization_plots(args_dict, gd_results, tel_model, data_to_plot, output_dir)
|
|
@@ -1309,8 +1449,8 @@ def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
|
1309
1449
|
gd_results,
|
|
1310
1450
|
threshold,
|
|
1311
1451
|
convergence_plot_file,
|
|
1312
|
-
|
|
1313
|
-
use_ks_statistic,
|
|
1452
|
+
optimizer.fraction,
|
|
1453
|
+
optimizer.use_ks_statistic,
|
|
1314
1454
|
)
|
|
1315
1455
|
|
|
1316
1456
|
param_file = write_gradient_descent_log(
|
|
@@ -1319,8 +1459,8 @@ def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
|
1319
1459
|
best_psf_diameter,
|
|
1320
1460
|
output_dir,
|
|
1321
1461
|
tel_model,
|
|
1322
|
-
use_ks_statistic,
|
|
1323
|
-
|
|
1462
|
+
optimizer.use_ks_statistic,
|
|
1463
|
+
optimizer.fraction,
|
|
1324
1464
|
)
|
|
1325
1465
|
logger.info(f"\nGradient descent progression written to {param_file}")
|
|
1326
1466
|
|
|
@@ -1331,3 +1471,6 @@ def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
|
1331
1471
|
export_psf_parameters(
|
|
1332
1472
|
best_pars, args_dict.get("telescope"), args_dict.get("parameter_version"), output_dir
|
|
1333
1473
|
)
|
|
1474
|
+
|
|
1475
|
+
if args_dict.get("cleanup", False):
|
|
1476
|
+
cleanup_intermediate_files(output_dir)
|