gammasimtools 0.25.0__py3-none-any.whl → 0.27.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.25.0.dist-info → gammasimtools-0.27.0.dist-info}/METADATA +6 -1
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/RECORD +135 -130
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/WHEEL +1 -1
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/entry_points.txt +3 -2
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/licenses/LICENSE +1 -1
- simtools/_version.py +2 -2
- simtools/application_control.py +35 -7
- simtools/applications/convert_geo_coordinates_of_array_elements.py +3 -3
- simtools/applications/db_add_file_to_db.py +1 -1
- simtools/applications/db_add_simulation_model_from_repository_to_db.py +1 -1
- simtools/applications/db_add_value_from_json_to_db.py +1 -1
- simtools/applications/db_generate_compound_indexes.py +1 -1
- simtools/applications/db_get_array_layouts_from_db.py +3 -7
- simtools/applications/db_get_file_from_db.py +1 -1
- simtools/applications/db_get_parameter_from_db.py +1 -1
- simtools/applications/db_inspect_databases.py +1 -1
- simtools/applications/db_upload_model_repository.py +1 -1
- simtools/applications/derive_ctao_array_layouts.py +1 -2
- simtools/applications/{calculate_incident_angles.py → derive_incident_angle.py} +16 -18
- simtools/applications/derive_mirror_rnda.py +112 -180
- simtools/applications/derive_psf_parameters.py +0 -1
- simtools/applications/derive_pulse_shape_parameters.py +0 -1
- simtools/applications/derive_trigger_rates.py +1 -1
- simtools/applications/docs_produce_array_element_report.py +2 -8
- simtools/applications/docs_produce_calibration_reports.py +1 -3
- simtools/applications/docs_produce_model_parameter_reports.py +0 -2
- simtools/applications/docs_produce_simulation_configuration_report.py +1 -3
- simtools/applications/generate_array_config.py +0 -1
- simtools/applications/generate_corsika_histograms.py +79 -229
- simtools/applications/generate_regular_arrays.py +76 -69
- simtools/applications/generate_simtel_event_data.py +2 -2
- simtools/applications/maintain_simulation_model_add_production.py +2 -2
- simtools/applications/maintain_simulation_model_write_array_element_positions.py +87 -0
- simtools/applications/plot_array_layout.py +5 -111
- simtools/applications/plot_simulated_event_distributions.py +57 -0
- simtools/applications/plot_tabular_data.py +0 -1
- simtools/applications/plot_tabular_data_for_model_parameter.py +1 -6
- simtools/applications/production_derive_corsika_limits.py +1 -1
- simtools/applications/production_generate_grid.py +0 -1
- simtools/applications/run_application.py +1 -1
- simtools/applications/simulate_flasher.py +3 -15
- simtools/applications/simulate_illuminator.py +2 -11
- simtools/applications/simulate_pedestals.py +1 -5
- simtools/applications/simulate_prod.py +8 -11
- simtools/applications/simulate_prod_htcondor_generator.py +1 -1
- simtools/applications/submit_array_layouts.py +2 -4
- simtools/applications/submit_data_from_external.py +2 -1
- simtools/applications/submit_model_parameter_from_external.py +1 -3
- simtools/applications/validate_camera_efficiency.py +28 -28
- simtools/applications/validate_camera_fov.py +0 -1
- simtools/applications/validate_cumulative_psf.py +1 -5
- simtools/applications/validate_optics.py +2 -14
- simtools/atmosphere.py +83 -0
- simtools/camera/camera_efficiency.py +171 -53
- simtools/camera/single_photon_electron_spectrum.py +8 -7
- simtools/configuration/commandline_parser.py +82 -11
- simtools/configuration/configurator.py +6 -11
- simtools/constants.py +5 -0
- simtools/corsika/corsika_config.py +100 -202
- simtools/corsika/corsika_histograms.py +561 -1708
- simtools/corsika/primary_particle.py +1 -1
- simtools/data_model/metadata_collector.py +5 -2
- simtools/data_model/metadata_model.py +0 -4
- simtools/data_model/model_data_writer.py +59 -64
- simtools/data_model/schema.py +2 -0
- simtools/data_model/validate_data.py +1 -3
- simtools/db/db_handler.py +23 -10
- simtools/db/mongo_db.py +2 -2
- simtools/dependencies.py +81 -38
- simtools/io/ascii_handler.py +55 -5
- simtools/io/io_handler.py +23 -12
- simtools/io/table_handler.py +1 -1
- simtools/job_execution/job_manager.py +154 -79
- simtools/job_execution/process_pool.py +137 -0
- simtools/layout/array_layout.py +4 -13
- simtools/layout/array_layout_utils.py +348 -57
- simtools/model/array_model.py +23 -63
- simtools/model/calibration_model.py +4 -8
- simtools/model/legacy_model_parameter.py +134 -0
- simtools/model/model_parameter.py +147 -86
- simtools/model/model_utils.py +40 -6
- simtools/model/site_model.py +4 -8
- simtools/model/telescope_model.py +10 -16
- simtools/production_configuration/derive_corsika_limits.py +6 -11
- simtools/production_configuration/interpolation_handler.py +16 -16
- simtools/ray_tracing/incident_angles.py +92 -17
- simtools/ray_tracing/mirror_panel_psf.py +338 -222
- simtools/ray_tracing/psf_analysis.py +62 -48
- simtools/ray_tracing/psf_parameter_optimisation.py +3 -3
- simtools/ray_tracing/ray_tracing.py +43 -25
- simtools/reporting/docs_auto_report_generator.py +8 -13
- simtools/reporting/docs_read_parameters.py +2 -8
- simtools/runners/corsika_runner.py +52 -195
- simtools/runners/corsika_simtel_runner.py +77 -108
- simtools/runners/runner_services.py +214 -213
- simtools/runners/simtel_runner.py +27 -160
- simtools/runners/simtools_runner.py +11 -73
- simtools/schemas/application_workflow.metaschema.yml +8 -0
- simtools/settings.py +173 -0
- simtools/{io/eventio_handler.py → sim_events/file_info.py} +3 -3
- simtools/{simtel/simtel_io_event_histograms.py → sim_events/histograms.py} +25 -15
- simtools/{simtel/simtel_io_event_reader.py → sim_events/reader.py} +20 -17
- simtools/{simtel/simtel_io_event_writer.py → sim_events/writer.py} +84 -25
- simtools/simtel/pulse_shapes.py +7 -2
- simtools/simtel/simtel_config_writer.py +79 -91
- simtools/simtel/simtel_seeds.py +184 -0
- simtools/simtel/simtel_table_reader.py +6 -4
- simtools/simtel/simulator_array.py +114 -109
- simtools/simtel/simulator_camera_efficiency.py +68 -46
- simtools/simtel/simulator_light_emission.py +164 -132
- simtools/simtel/simulator_ray_tracing.py +80 -71
- simtools/simulator.py +137 -355
- simtools/telescope_trigger_rates.py +3 -4
- simtools/testing/assertions.py +84 -33
- simtools/testing/configuration.py +1 -2
- simtools/testing/helpers.py +2 -3
- simtools/testing/log_inspector.py +1 -0
- simtools/testing/sim_telarray_metadata.py +14 -12
- simtools/testing/validate_output.py +121 -42
- simtools/utils/general.py +43 -17
- simtools/utils/geometry.py +0 -77
- simtools/utils/names.py +5 -5
- simtools/utils/random.py +36 -0
- simtools/visualization/legend_handlers.py +7 -6
- simtools/visualization/plot_array_layout.py +91 -16
- simtools/visualization/plot_corsika_histograms.py +145 -605
- simtools/visualization/plot_incident_angles.py +48 -1
- simtools/visualization/plot_mirrors.py +1 -4
- simtools/visualization/plot_pixels.py +2 -4
- simtools/visualization/plot_psf.py +160 -19
- simtools/visualization/plot_simtel_event_histograms.py +4 -4
- simtools/visualization/plot_simtel_events.py +6 -11
- simtools/visualization/plot_tables.py +8 -19
- simtools/visualization/visualize.py +22 -2
- simtools/applications/db_development_tools/write_array_elements_positions_to_repository.py +0 -160
- simtools/applications/print_version.py +0 -53
- simtools/io/hdf5_handler.py +0 -139
- {gammasimtools-0.25.0.dist-info → gammasimtools-0.27.0.dist-info}/top_level.txt +0 -0
|
@@ -1,297 +1,413 @@
|
|
|
1
|
-
"""Mirror panel PSF calculation."""
|
|
1
|
+
"""Mirror panel PSF calculation with per-mirror PSF diameter optimization."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from pathlib import Path
|
|
4
8
|
|
|
5
|
-
import astropy.units as u
|
|
6
9
|
import numpy as np
|
|
7
|
-
from astropy.table import
|
|
10
|
+
from astropy.table import Table
|
|
8
11
|
|
|
9
|
-
import simtools.data_model.model_data_writer as writer
|
|
10
12
|
import simtools.utils.general as gen
|
|
11
|
-
from simtools.data_model
|
|
13
|
+
from simtools.data_model import model_data_writer
|
|
14
|
+
from simtools.job_execution.process_pool import process_pool_map_ordered
|
|
12
15
|
from simtools.model.model_utils import initialize_simulation_models
|
|
13
16
|
from simtools.ray_tracing.ray_tracing import RayTracing
|
|
17
|
+
from simtools.utils import names
|
|
18
|
+
from simtools.visualization import plot_psf
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class MirrorOptimizationResult:
|
|
23
|
+
"""Dataclass to store the result of a single mirror RNDA optimization."""
|
|
24
|
+
|
|
25
|
+
mirror: int # Zero-based mirror index
|
|
26
|
+
measured_psf_mm: float # Measured PSF diameter in mm
|
|
27
|
+
optimized_rnda: list[float] # Optimized RNDA values [sigma1, fraction2, sigma2]
|
|
28
|
+
simulated_psf_mm: float # Simulated PSF diameter after optimization
|
|
29
|
+
percentage_diff: float # Absolute percentage difference
|
|
30
|
+
containment_fraction: float # PSF containment fraction used for the diameter
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _optimize_single_mirror_worker(args):
|
|
34
|
+
"""Worker wrapper for optimizing a single mirror in parallel."""
|
|
35
|
+
instance, mirror_idx, measured = args
|
|
36
|
+
return instance.optimize_single_mirror(mirror_idx, measured)
|
|
14
37
|
|
|
15
38
|
|
|
16
39
|
class MirrorPanelPSF:
|
|
17
40
|
"""
|
|
18
|
-
Mirror panel PSF and random reflection angle calculation.
|
|
41
|
+
Mirror panel PSF and random reflection angle (RNDA) calculation.
|
|
19
42
|
|
|
20
|
-
This class
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
This class derives the RNDA for mirror panels by optimizing
|
|
44
|
+
per-mirror PSF diameters (at a given containment fraction)
|
|
45
|
+
using a percentage difference metric.
|
|
46
|
+
Optimization uses a gradient descent with finite-difference gradients.
|
|
24
47
|
|
|
25
48
|
Parameters
|
|
26
49
|
----------
|
|
27
|
-
label: str
|
|
50
|
+
label : str
|
|
28
51
|
Application label.
|
|
29
|
-
args_dict: dict
|
|
30
|
-
Dictionary with input arguments.
|
|
31
|
-
db_config:
|
|
32
|
-
Dictionary with database configuration.
|
|
52
|
+
args_dict : dict
|
|
53
|
+
Dictionary with input arguments, e.g. site, telescope, data path, model version.
|
|
33
54
|
"""
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
# Hard guardrails
|
|
57
|
+
GRAD_CLIP = 1e4
|
|
58
|
+
MAX_LOG_STEP = 0.25
|
|
59
|
+
MAX_FRAC_STEP = 0.1
|
|
60
|
+
MAX_ITER = 100
|
|
39
61
|
|
|
62
|
+
def __init__(self, label, args_dict):
|
|
63
|
+
self._logger = logging.getLogger(__name__)
|
|
64
|
+
self.label = label
|
|
40
65
|
self.args_dict = args_dict
|
|
41
|
-
self.telescope_model, self.site_model = self._define_telescope_model(label, db_config)
|
|
42
66
|
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
self.fraction = args_dict.get("fraction", 0.8)
|
|
68
|
+
|
|
69
|
+
self.telescope_model, self.site_model, _ = initialize_simulation_models(
|
|
70
|
+
label=label,
|
|
71
|
+
site=args_dict["site"],
|
|
72
|
+
telescope_name=args_dict["telescope"],
|
|
73
|
+
model_version=args_dict["model_version"],
|
|
74
|
+
)
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
self.measured_data = self._load_measured_data()
|
|
77
|
+
self.rnda_start = list(
|
|
78
|
+
self.telescope_model.get_parameter_value("mirror_reflection_random_angle")
|
|
79
|
+
)
|
|
50
80
|
|
|
51
|
-
self.
|
|
52
|
-
self.sig_d80 = None
|
|
53
|
-
self.rnda_start = self._get_starting_value()
|
|
81
|
+
self.per_mirror_results = []
|
|
54
82
|
self.rnda_opt = None
|
|
55
|
-
self.
|
|
56
|
-
self.results_mean = []
|
|
57
|
-
self.results_sig = []
|
|
83
|
+
self.final_percentage_diff = None
|
|
58
84
|
|
|
59
|
-
def
|
|
85
|
+
def _load_measured_data(self):
|
|
60
86
|
"""
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
This includes updating the configuration with mirror list and/or random focal length given
|
|
64
|
-
as input.
|
|
87
|
+
Load measured PSF diameter from ECSV file.
|
|
65
88
|
|
|
66
89
|
Returns
|
|
67
90
|
-------
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
site_model : SiteModel
|
|
71
|
-
The site model.
|
|
91
|
+
astropy.table.Column
|
|
92
|
+
Column containing PSF diameter values in mm.
|
|
72
93
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
tel_model.overwrite_model_parameter(
|
|
88
|
-
"random_focal_length", str(self.args_dict["random_focal_length"])
|
|
89
|
-
)
|
|
94
|
+
data_file = gen.find_file(self.args_dict["data"], self.args_dict.get("model_path", "."))
|
|
95
|
+
table = Table.read(data_file)
|
|
96
|
+
if "psf_opt" in table.colnames:
|
|
97
|
+
return table["psf_opt"]
|
|
98
|
+
if "d80" in table.colnames:
|
|
99
|
+
fraction = float(self.args_dict.get("fraction", 0.8))
|
|
100
|
+
if not np.isclose(fraction, 0.8, rtol=1e-09, atol=1e-09):
|
|
101
|
+
self._logger.warning(
|
|
102
|
+
"Input table provides 'd80' column, but --fraction=%.3f was requested. "
|
|
103
|
+
"Make sure the measured column matches the selected containment fraction.",
|
|
104
|
+
fraction,
|
|
105
|
+
)
|
|
106
|
+
return table["d80"]
|
|
107
|
+
raise ValueError("Data file must contain either 'psf_opt' or 'd80' column")
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
def _simulate_single_mirror_psf(self, mirror_idx, rnda_values):
|
|
110
|
+
"""
|
|
111
|
+
Simulate a single mirror and return its PSF diameter.
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
mirror_idx : int
|
|
116
|
+
Zero-based index of the mirror.
|
|
117
|
+
rnda_values : list of float
|
|
118
|
+
RNDA values [sigma1, fraction2, sigma2].
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
float
|
|
123
|
+
Simulated PSF diameter in mm.
|
|
124
|
+
"""
|
|
125
|
+
fraction = self.fraction
|
|
126
|
+
self.telescope_model.overwrite_model_parameter(
|
|
127
|
+
"mirror_reflection_random_angle", rnda_values
|
|
101
128
|
)
|
|
102
|
-
try:
|
|
103
|
-
self.args_dict["psf_measurement_containment_mean"] = np.nanmean(
|
|
104
|
-
np.array(_psf_list["psf_opt"].to("cm").value)
|
|
105
|
-
)
|
|
106
|
-
self.args_dict["psf_measurement_containment_sigma"] = np.nanstd(
|
|
107
|
-
np.array(_psf_list["psf_opt"].to("cm").value)
|
|
108
|
-
)
|
|
109
|
-
except KeyError as exc:
|
|
110
|
-
raise KeyError(
|
|
111
|
-
f"Missing column psf measurement (psf_opt) in {self.args_dict['psf_measurement']}"
|
|
112
|
-
) from exc
|
|
113
129
|
|
|
114
|
-
self.
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
rt_label = f"{self.label}_m{mirror_idx}"
|
|
131
|
+
ray = RayTracing(
|
|
132
|
+
telescope_model=self.telescope_model,
|
|
133
|
+
site_model=self.site_model,
|
|
134
|
+
label=rt_label,
|
|
135
|
+
single_mirror_mode=True,
|
|
136
|
+
mirror_numbers=[mirror_idx],
|
|
117
137
|
)
|
|
138
|
+
ray.simulate(test=self.args_dict.get("test", False), force=True)
|
|
139
|
+
ray.analyze(force=True, containment_fraction=fraction)
|
|
140
|
+
|
|
141
|
+
return float(ray.get_psf_mm())
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _signed_pct_diff(measured, simulated):
|
|
145
|
+
"""Compute signed percentage difference."""
|
|
146
|
+
if measured <= 0:
|
|
147
|
+
raise ValueError("Measured PSF diameter must be positive")
|
|
148
|
+
return 100.0 * (simulated - measured) / measured
|
|
118
149
|
|
|
119
|
-
def
|
|
150
|
+
def _evaluate(self, mirror_idx, measured, rnda):
|
|
120
151
|
"""
|
|
121
|
-
|
|
152
|
+
Evaluate the objective function for a mirror.
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
mirror_idx : int
|
|
157
|
+
Mirror index (0-based).
|
|
158
|
+
measured : float
|
|
159
|
+
Measured PSF diameter (mm).
|
|
160
|
+
rnda : list of float
|
|
161
|
+
Current RNDA values.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
tuple of (float, float, float)
|
|
166
|
+
Simulated PSF diameter, signed percentage difference, squared percentage difference.
|
|
126
167
|
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
self._optimize_reflection_angle()
|
|
168
|
+
sim = self._simulate_single_mirror_psf(mirror_idx, rnda)
|
|
169
|
+
pct = self._signed_pct_diff(measured, sim)
|
|
170
|
+
return sim, pct, pct * pct
|
|
131
171
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _rnda_bounds():
|
|
174
|
+
"""
|
|
175
|
+
Get allowed bounds for RNDA parameters.
|
|
135
176
|
|
|
136
|
-
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
list of tuple of float
|
|
180
|
+
[(min1, max1), (min2, max2), (min3, max3)]
|
|
137
181
|
"""
|
|
138
|
-
|
|
182
|
+
schema = names.model_parameters()["mirror_reflection_random_angle"]["data"]
|
|
183
|
+
return [
|
|
184
|
+
(max(schema[0]["allowed_range"]["min"], 1e-12), schema[0]["allowed_range"]["max"]),
|
|
185
|
+
(schema[1]["allowed_range"]["min"], schema[1]["allowed_range"]["max"]),
|
|
186
|
+
(max(schema[2]["allowed_range"]["min"], 1e-12), schema[2]["allowed_range"]["max"]),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def optimize_single_mirror(self, mirror_idx, measured_psf):
|
|
190
|
+
"""
|
|
191
|
+
Optimize RNDA for a single mirror using gradient descent.
|
|
139
192
|
|
|
140
193
|
Parameters
|
|
141
194
|
----------
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Raises
|
|
148
|
-
------
|
|
149
|
-
ValueError
|
|
150
|
-
If the optimization reaches the maximum number of iterations without converging.
|
|
195
|
+
mirror_idx : int
|
|
196
|
+
Zero-based mirror index.
|
|
197
|
+
measured_psf : float
|
|
198
|
+
Measured PSF diameter in mm.
|
|
151
199
|
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
MirrorOptimizationResult
|
|
203
|
+
Optimization results for the mirror.
|
|
152
204
|
"""
|
|
153
|
-
|
|
205
|
+
threshold_pct = 100 * float(self.args_dict.get("threshold", 0.05))
|
|
206
|
+
learning_rate = float(self.args_dict.get("learning_rate", 1e-3))
|
|
207
|
+
bounds = self._rnda_bounds()
|
|
208
|
+
fraction = self.fraction
|
|
209
|
+
|
|
210
|
+
rnda = list(self.rnda_start)
|
|
211
|
+
sim, pct, obj = self._evaluate(mirror_idx, measured_psf, rnda)
|
|
212
|
+
best = {"rnda": list(rnda), "sim": sim, "pct": abs(pct), "obj": obj}
|
|
213
|
+
|
|
154
214
|
self._logger.info(
|
|
155
|
-
"
|
|
156
|
-
|
|
157
|
-
|
|
215
|
+
"Mirror %d | initial PSF %.3f mm (f=%.2f) | pct %.2f%%",
|
|
216
|
+
mirror_idx,
|
|
217
|
+
sim,
|
|
218
|
+
fraction,
|
|
219
|
+
pct,
|
|
158
220
|
)
|
|
159
221
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
222
|
+
for iteration in range(self.MAX_ITER):
|
|
223
|
+
old_rnda = list(rnda)
|
|
224
|
+
for param_idx, (param_value, param_bounds) in enumerate(zip(old_rnda, bounds)):
|
|
225
|
+
rnda[param_idx] = self._update_single_rnda_parameter(
|
|
226
|
+
mirror_idx=mirror_idx,
|
|
227
|
+
measured_psf=measured_psf,
|
|
228
|
+
rnda=rnda,
|
|
229
|
+
param_idx=param_idx,
|
|
230
|
+
param_value=param_value,
|
|
231
|
+
param_bounds=param_bounds,
|
|
232
|
+
learning_rate=learning_rate,
|
|
233
|
+
)
|
|
164
234
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
prev_error_d80 = float("inf")
|
|
168
|
-
iteration = 0
|
|
235
|
+
sim, pct, obj = self._evaluate(mirror_idx, measured_psf, rnda)
|
|
236
|
+
pct = abs(pct)
|
|
169
237
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
error_d80 = abs(1 - mean_d80 / reference_d80)
|
|
173
|
-
collect_results(rnda, mean_d80, sig_d80)
|
|
238
|
+
old_rnda_str = "[" + ", ".join(f"{v:.4g}" for v in old_rnda) + "]"
|
|
239
|
+
rnda_str = "[" + ", ".join(f"{v:.4g}" for v in rnda) + "]"
|
|
174
240
|
|
|
175
|
-
|
|
176
|
-
|
|
241
|
+
self._logger.info(
|
|
242
|
+
"Iter %d | rnda %s -> %s | pct %.2f -> %.2f | lr %.2g",
|
|
243
|
+
iteration + 1,
|
|
244
|
+
old_rnda_str,
|
|
245
|
+
rnda_str,
|
|
246
|
+
best["pct"],
|
|
247
|
+
pct,
|
|
248
|
+
learning_rate,
|
|
249
|
+
)
|
|
177
250
|
|
|
178
|
-
if
|
|
179
|
-
rnda
|
|
251
|
+
if obj < best["obj"]:
|
|
252
|
+
best.update(rnda=list(rnda), sim=sim, pct=pct, obj=obj)
|
|
253
|
+
learning_rate *= 1.1
|
|
180
254
|
else:
|
|
181
|
-
rnda
|
|
182
|
-
|
|
183
|
-
if error_d80 >= prev_error_d80:
|
|
184
|
-
step_size = step_size / 2
|
|
185
|
-
prev_error_d80 = error_d80
|
|
186
|
-
iteration += 1
|
|
187
|
-
if iteration > max_iteration:
|
|
188
|
-
raise ValueError(
|
|
189
|
-
f"Maximum iterations ({max_iteration}) reached without convergence."
|
|
190
|
-
)
|
|
255
|
+
rnda = list(old_rnda)
|
|
256
|
+
learning_rate *= 0.5
|
|
191
257
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def _get_starting_value(self):
|
|
195
|
-
"""Get optimization starting value from command line or previous model."""
|
|
196
|
-
if self.args_dict["rnda"] != 0:
|
|
197
|
-
rnda_start = self.args_dict["rnda"]
|
|
198
|
-
else:
|
|
199
|
-
rnda_start = self.telescope_model.get_parameter_value("mirror_reflection_random_angle")[
|
|
200
|
-
0
|
|
201
|
-
]
|
|
258
|
+
if best["pct"] <= threshold_pct or learning_rate < 1e-12:
|
|
259
|
+
break
|
|
202
260
|
|
|
203
|
-
|
|
204
|
-
|
|
261
|
+
return MirrorOptimizationResult(
|
|
262
|
+
mirror=mirror_idx,
|
|
263
|
+
measured_psf_mm=float(measured_psf),
|
|
264
|
+
optimized_rnda=best["rnda"],
|
|
265
|
+
simulated_psf_mm=best["sim"],
|
|
266
|
+
percentage_diff=best["pct"],
|
|
267
|
+
containment_fraction=fraction,
|
|
268
|
+
)
|
|
205
269
|
|
|
206
|
-
def
|
|
270
|
+
def _update_single_rnda_parameter(
|
|
271
|
+
self,
|
|
272
|
+
mirror_idx,
|
|
273
|
+
measured_psf,
|
|
274
|
+
rnda,
|
|
275
|
+
param_idx,
|
|
276
|
+
param_value,
|
|
277
|
+
param_bounds,
|
|
278
|
+
learning_rate,
|
|
279
|
+
):
|
|
207
280
|
"""
|
|
208
|
-
|
|
281
|
+
Update a single RNDA parameter using finite-difference gradient descent.
|
|
209
282
|
|
|
210
283
|
Parameters
|
|
211
284
|
----------
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
285
|
+
mirror_idx : int
|
|
286
|
+
measured_psf : float
|
|
287
|
+
rnda : list of float
|
|
288
|
+
param_idx : int
|
|
289
|
+
param_value : float
|
|
290
|
+
param_bounds : tuple of float
|
|
291
|
+
learning_rate : float
|
|
216
292
|
|
|
217
293
|
Returns
|
|
218
294
|
-------
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
sig_d80: float
|
|
222
|
-
Standard deviation of D80 in cm.
|
|
295
|
+
float
|
|
296
|
+
Updated RNDA parameter value.
|
|
223
297
|
"""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
site_model=self.site_model,
|
|
228
|
-
simtel_path=self.args_dict.get("simtel_path", None),
|
|
229
|
-
single_mirror_mode=True,
|
|
230
|
-
mirror_numbers=(
|
|
231
|
-
list(range(1, self.args_dict["number_of_mirrors_to_test"] + 1))
|
|
232
|
-
if self.args_dict["test"]
|
|
233
|
-
else "all"
|
|
234
|
-
),
|
|
235
|
-
use_random_focal_length=self.args_dict["use_random_focal_length"],
|
|
236
|
-
random_focal_length_seed=self.args_dict.get("random_focal_length_seed"),
|
|
237
|
-
)
|
|
238
|
-
ray.simulate(test=self.args_dict["test"], force=True) # force has to be True, always
|
|
239
|
-
ray.analyze(force=True)
|
|
240
|
-
if save_figures:
|
|
241
|
-
ray.plot("d80_cm", save=True, d80=self.args_dict["psf_measurement_containment_mean"])
|
|
242
|
-
|
|
243
|
-
return (
|
|
244
|
-
ray.get_mean("d80_cm").to(u.cm).value,
|
|
245
|
-
ray.get_std_dev("d80_cm").to(u.cm).value,
|
|
246
|
-
)
|
|
298
|
+
param_min, param_max = param_bounds
|
|
299
|
+
is_log_param = param_idx != 1
|
|
300
|
+
epsilon = max(1e-6, 0.05 * param_value) if is_log_param else 0.05
|
|
247
301
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
containment_fraction_percent = int(self.args_dict["containment_fraction"] * 100)
|
|
302
|
+
rnda[param_idx] = min(param_max, param_value + epsilon)
|
|
303
|
+
_, _, f_plus = self._evaluate(mirror_idx, measured_psf, rnda)
|
|
251
304
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
print(
|
|
255
|
-
f"Mean = {self.args_dict['psf_measurement_containment_mean']:.3f} cm, "
|
|
256
|
-
f"StdDev = {self.args_dict['psf_measurement_containment_sigma']:.3f} cm"
|
|
257
|
-
)
|
|
258
|
-
else:
|
|
259
|
-
print(f"Mean = {self.args_dict['psf_measurement_containment_mean']:.3f} cm")
|
|
260
|
-
print(f"\nSimulated D{containment_fraction_percent}:")
|
|
261
|
-
print(f"Mean = {self.mean_d80:.3f} cm, StdDev = {self.sig_d80:.3f} cm")
|
|
262
|
-
print("\nmirror_random_reflection_angle")
|
|
263
|
-
print(f"Previous value = {self.rnda_start:.6f}")
|
|
264
|
-
print(f"New value = {self.rnda_opt:.6f}\n")
|
|
305
|
+
rnda[param_idx] = max(param_min, param_value - epsilon)
|
|
306
|
+
_, _, f_minus = self._evaluate(mirror_idx, measured_psf, rnda)
|
|
265
307
|
|
|
266
|
-
|
|
308
|
+
rnda[param_idx] = param_value
|
|
309
|
+
gradient = (f_plus - f_minus) / (2 * epsilon)
|
|
310
|
+
|
|
311
|
+
# Clip the gradient to avoid excessively large steps.
|
|
312
|
+
if is_log_param:
|
|
313
|
+
gradient = np.clip(gradient * param_value, -self.GRAD_CLIP, self.GRAD_CLIP)
|
|
314
|
+
step = np.clip(-learning_rate * gradient, -self.MAX_LOG_STEP, self.MAX_LOG_STEP)
|
|
315
|
+
# clip the parameters so they are within allowed range
|
|
316
|
+
return np.clip(param_value * np.exp(step), param_min, param_max)
|
|
317
|
+
|
|
318
|
+
gradient = np.clip(gradient, -self.GRAD_CLIP, self.GRAD_CLIP)
|
|
319
|
+
step = np.clip(-learning_rate * gradient, -self.MAX_FRAC_STEP, self.MAX_FRAC_STEP)
|
|
320
|
+
return np.clip(param_value + step, param_min, param_max)
|
|
321
|
+
|
|
322
|
+
def optimize_with_gradient_descent(self):
|
|
267
323
|
"""
|
|
268
|
-
|
|
324
|
+
Optimize all mirrors in parallel using process pool.
|
|
269
325
|
|
|
270
|
-
|
|
271
|
-
|
|
326
|
+
Sets
|
|
327
|
+
----
|
|
328
|
+
self.per_mirror_results : list of dict
|
|
329
|
+
self.rnda_opt : list of float
|
|
330
|
+
self.final_percentage_diff : float
|
|
272
331
|
"""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
"mirror_reflection_random_angle_sigma1",
|
|
287
|
-
"mirror_reflection_random_angle_fraction2",
|
|
288
|
-
"mirror_reflection_random_angle_sigma2",
|
|
289
|
-
f"containment_radius_D{containment_fraction_percent}",
|
|
290
|
-
f"containment_radius_sigma_D{containment_fraction_percent}",
|
|
291
|
-
),
|
|
332
|
+
n_mirrors = len(self.measured_data)
|
|
333
|
+
if self.args_dict.get("test"):
|
|
334
|
+
n_mirrors = min(n_mirrors, self.args_dict.get("number_of_mirrors_to_test"))
|
|
335
|
+
|
|
336
|
+
n_workers = int(self.args_dict.get("n_workers") or os.cpu_count())
|
|
337
|
+
parent = MirrorPanelPSF(self.label, dict(self.args_dict))
|
|
338
|
+
worker_args = [(parent, i, parent.measured_data[i]) for i in range(n_mirrors)]
|
|
339
|
+
|
|
340
|
+
self.per_mirror_results = process_pool_map_ordered(
|
|
341
|
+
_optimize_single_mirror_worker,
|
|
342
|
+
worker_args,
|
|
343
|
+
max_workers=n_workers,
|
|
344
|
+
mp_start_method="fork",
|
|
292
345
|
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
346
|
+
|
|
347
|
+
self.rnda_opt = np.mean(
|
|
348
|
+
[r.optimized_rnda for r in self.per_mirror_results], axis=0
|
|
349
|
+
).tolist()
|
|
350
|
+
|
|
351
|
+
self.final_percentage_diff = float(
|
|
352
|
+
np.mean([r.percentage_diff for r in self.per_mirror_results])
|
|
297
353
|
)
|
|
354
|
+
|
|
355
|
+
def write_optimization_data(self):
|
|
356
|
+
"""Write optimization results and optionally export as a model parameter."""
|
|
357
|
+
output_dir = Path(self.args_dict.get("output_path", "."))
|
|
358
|
+
telescope = self.args_dict.get("telescope")
|
|
359
|
+
parameter_version = self.args_dict.get("parameter_version")
|
|
360
|
+
parameter_name = "mirror_reflection_random_angle"
|
|
361
|
+
parameter_output_path = output_dir / str(telescope)
|
|
362
|
+
parameter_output_path.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
output_file = parameter_output_path / "per_mirror_rnda.json"
|
|
364
|
+
|
|
365
|
+
per_mirror_results_out = []
|
|
366
|
+
for result in self.per_mirror_results:
|
|
367
|
+
result_dict = asdict(result)
|
|
368
|
+
for k, v in result_dict.items():
|
|
369
|
+
if k == "optimized_rnda":
|
|
370
|
+
result_dict[k] = [float(f"{x:.4f}") for x in v]
|
|
371
|
+
elif isinstance(v, (int, float)):
|
|
372
|
+
result_dict[k] = float(f"{v:.4f}")
|
|
373
|
+
per_mirror_results_out.append(result_dict)
|
|
374
|
+
|
|
375
|
+
output_file.write_text(
|
|
376
|
+
json.dumps(
|
|
377
|
+
{
|
|
378
|
+
"telescope": telescope,
|
|
379
|
+
"model_version": self.args_dict.get("model_version"),
|
|
380
|
+
"per_mirror_results": per_mirror_results_out,
|
|
381
|
+
},
|
|
382
|
+
indent=2,
|
|
383
|
+
),
|
|
384
|
+
encoding="utf-8",
|
|
385
|
+
)
|
|
386
|
+
self._logger.info("Results written to %s", output_file)
|
|
387
|
+
|
|
388
|
+
# Export averaged RNDA
|
|
389
|
+
if telescope and parameter_version and self.rnda_opt is not None:
|
|
390
|
+
try:
|
|
391
|
+
model_data_writer.ModelDataWriter.dump_model_parameter(
|
|
392
|
+
parameter_name=parameter_name,
|
|
393
|
+
value=[float(f"{v:.4f}") for v in self.rnda_opt],
|
|
394
|
+
instrument=str(telescope),
|
|
395
|
+
parameter_version=str(parameter_version),
|
|
396
|
+
output_file=f"{parameter_name}-{parameter_version}.json",
|
|
397
|
+
output_path=parameter_output_path,
|
|
398
|
+
unit=["deg", "dimensionless", "deg"],
|
|
399
|
+
)
|
|
400
|
+
self._logger.info(
|
|
401
|
+
"Exported model parameter %s (%s) to %s",
|
|
402
|
+
parameter_name,
|
|
403
|
+
parameter_version,
|
|
404
|
+
parameter_output_path,
|
|
405
|
+
)
|
|
406
|
+
except (OSError, ValueError, TypeError) as e:
|
|
407
|
+
self._logger.warning("Failed to export model parameter %s: %s", parameter_name, e)
|
|
408
|
+
|
|
409
|
+
def write_psf_histogram(self):
|
|
410
|
+
"""Plot histogram of measured vs simulated psf values."""
|
|
411
|
+
measured = [r.measured_psf_mm for r in self.per_mirror_results]
|
|
412
|
+
simulated = [r.simulated_psf_mm for r in self.per_mirror_results]
|
|
413
|
+
return plot_psf.plot_psf_histogram(measured, simulated, self.args_dict)
|