gammasimtools 0.20.0__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/METADATA +2 -3
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/RECORD +313 -296
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/entry_points.txt +3 -2
- simtools/_version.py +2 -2
- simtools/applications/calculate_incident_angles.py +1 -4
- simtools/applications/convert_all_model_parameters_from_simtel.py +1 -2
- simtools/applications/convert_model_parameter_from_simtel.py +0 -1
- simtools/applications/db_generate_compound_indexes.py +4 -17
- simtools/applications/db_upload_model_repository.py +122 -0
- simtools/applications/derive_psf_parameters.py +71 -42
- simtools/applications/docs_produce_array_element_report.py +1 -1
- simtools/applications/docs_produce_calibration_reports.py +1 -1
- simtools/applications/docs_produce_model_parameter_reports.py +1 -1
- simtools/applications/docs_produce_simulation_configuration_report.py +1 -1
- simtools/applications/generate_corsika_histograms.py +8 -185
- simtools/applications/maintain_simulation_model_add_production.py +81 -0
- simtools/applications/merge_tables.py +1 -1
- simtools/applications/plot_array_layout.py +1 -2
- simtools/applications/plot_simtel_events.py +2 -228
- simtools/applications/print_version.py +8 -7
- simtools/applications/production_derive_statistics.py +1 -2
- simtools/applications/production_generate_grid.py +1 -1
- simtools/applications/simulate_flasher.py +74 -72
- simtools/applications/simulate_illuminator.py +52 -186
- simtools/applications/{simulate_calibration_events.py → simulate_pedestals.py} +9 -55
- simtools/applications/submit_model_parameter_from_external.py +0 -1
- simtools/applications/validate_camera_efficiency.py +0 -1
- simtools/applications/validate_camera_fov.py +1 -2
- simtools/applications/validate_cumulative_psf.py +2 -3
- simtools/applications/validate_file_using_schema.py +20 -12
- simtools/applications/validate_optics.py +2 -2
- simtools/camera/camera_efficiency.py +8 -11
- simtools/configuration/commandline_parser.py +1 -7
- simtools/configuration/configurator.py +0 -2
- simtools/corsika/corsika_config.py +9 -11
- simtools/corsika/corsika_histograms.py +82 -1
- simtools/data_model/model_data_writer.py +87 -25
- simtools/data_model/schema.py +61 -2
- simtools/data_model/validate_data.py +1 -1
- simtools/db/db_handler.py +103 -48
- simtools/db/db_model_upload.py +247 -16
- simtools/io/io_handler.py +31 -83
- simtools/job_execution/job_manager.py +45 -0
- simtools/layout/array_layout_utils.py +1 -5
- simtools/model/array_model.py +93 -42
- simtools/model/model_parameter.py +20 -9
- simtools/model/model_repository.py +197 -109
- simtools/model/model_utils.py +21 -6
- simtools/model/telescope_model.py +20 -0
- simtools/production_configuration/derive_corsika_limits.py +1 -1
- simtools/ray_tracing/incident_angles.py +7 -7
- simtools/ray_tracing/mirror_panel_psf.py +1 -1
- simtools/ray_tracing/psf_parameter_optimisation.py +1106 -565
- simtools/ray_tracing/ray_tracing.py +1 -3
- simtools/reporting/docs_read_parameters.py +171 -101
- simtools/resources/array_elements.yml +26 -0
- simtools/runners/corsika_simtel_runner.py +11 -17
- simtools/runners/runner_services.py +5 -6
- simtools/runners/simtools_runner.py +0 -2
- simtools/schemas/application_workflow.metaschema.yml +1 -1
- simtools/schemas/common_definitions.schema.yml +39 -0
- simtools/schemas/model_parameter.metaschema.yml +19 -13
- simtools/schemas/model_parameter_and_data_schema.metaschema.yml +6 -12
- simtools/schemas/model_parameters/adjust_gain.schema.yml +0 -5
- simtools/schemas/model_parameters/altitude.schema.yml +0 -5
- simtools/schemas/model_parameters/array_coordinates.schema.yml +0 -5
- simtools/schemas/model_parameters/array_coordinates_UTM.schema.yml +0 -5
- simtools/schemas/model_parameters/array_element_position_ground.schema.yml +0 -7
- simtools/schemas/model_parameters/array_element_position_utm.schema.yml +0 -7
- simtools/schemas/model_parameters/array_layouts.schema.yml +0 -5
- simtools/schemas/model_parameters/array_triggers.schema.yml +0 -5
- simtools/schemas/model_parameters/array_window.schema.yml +0 -7
- simtools/schemas/model_parameters/asum_clipping.schema.yml +0 -3
- simtools/schemas/model_parameters/asum_offset.schema.yml +0 -7
- simtools/schemas/model_parameters/asum_shaping.schema.yml +0 -7
- simtools/schemas/model_parameters/asum_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/atmospheric_profile.schema.yml +0 -5
- simtools/schemas/model_parameters/atmospheric_transmission.schema.yml +0 -5
- simtools/schemas/model_parameters/axes_offsets.schema.yml +0 -7
- simtools/schemas/model_parameters/calibration_devices.schema.yml +30 -0
- simtools/schemas/model_parameters/camera_body_diameter.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_body_shape.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_config_file.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_config_rotate.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_degraded_efficiency.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_degraded_map.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_depth.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_filter.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_filter_incidence_angle.schema.yml +0 -3
- simtools/schemas/model_parameters/camera_pixels.schema.yml +0 -7
- simtools/schemas/model_parameters/camera_transmission.schema.yml +0 -7
- simtools/schemas/model_parameters/channels_per_chip.schema.yml +0 -7
- simtools/schemas/model_parameters/correct_nsb_spectrum_to_telescope_altitude.schema.yml +0 -7
- simtools/schemas/model_parameters/corsika_observation_level.schema.yml +0 -5
- simtools/schemas/model_parameters/dark_events.schema.yml +4 -3
- simtools/schemas/model_parameters/default_trigger.schema.yml +0 -7
- simtools/schemas/model_parameters/design_model.schema.yml +0 -7
- simtools/schemas/model_parameters/disc_ac_coupled.schema.yml +0 -7
- simtools/schemas/model_parameters/disc_bins.schema.yml +0 -7
- simtools/schemas/model_parameters/disc_start.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_amplitude.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_fall_time.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_gate_length.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_hysteresis.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_output_amplitude.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_output_var_percent.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_pulse_shape.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_rise_time.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_scale_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_sigsum_over_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_time_over_threshold.schema.yml +1 -9
- simtools/schemas/model_parameters/discriminator_var_gate_length.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_var_sigsum_over_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_var_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/discriminator_var_time_over_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/dish_shape_length.schema.yml +0 -5
- simtools/schemas/model_parameters/dsum_clipping.schema.yml +1 -5
- simtools/schemas/model_parameters/dsum_ignore_below.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_offset.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_pedsub.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_pre_clipping.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_prescale.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_presum_max.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_presum_shift.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_shaping.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_shaping_renormalize.schema.yml +0 -3
- simtools/schemas/model_parameters/dsum_threshold.schema.yml +2 -12
- simtools/schemas/model_parameters/dsum_zero_clip.schema.yml +0 -3
- simtools/schemas/model_parameters/effective_focal_length.schema.yml +0 -7
- simtools/schemas/model_parameters/epsg_code.schema.yml +0 -5
- simtools/schemas/model_parameters/fadc_ac_coupled.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_amplitude.schema.yml +2 -9
- simtools/schemas/model_parameters/fadc_bins.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_compensate_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_dev_pedestal.schema.yml +0 -2
- simtools/schemas/model_parameters/fadc_err_compensate_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_err_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_amplitude.schema.yml +2 -9
- simtools/schemas/model_parameters/fadc_lg_compensate_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_dev_pedestal.schema.yml +0 -2
- simtools/schemas/model_parameters/fadc_lg_err_compensate_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_err_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_max_signal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_max_sum.schema.yml +0 -2
- simtools/schemas/model_parameters/fadc_lg_noise.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_sensitivity.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_sysvar_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_var_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_lg_var_sensitivity.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_long_event_threshold.schema.yml +0 -3
- simtools/schemas/model_parameters/fadc_long_sum_bins.schema.yml +0 -3
- simtools/schemas/model_parameters/fadc_long_sum_offset.schema.yml +0 -3
- simtools/schemas/model_parameters/fadc_max_signal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_max_sum.schema.yml +0 -2
- simtools/schemas/model_parameters/fadc_mhz.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_noise.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_pulse_shape.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_sensitivity.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_sum_bins.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_sum_offset.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_sysvar_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_var_pedestal.schema.yml +0 -7
- simtools/schemas/model_parameters/fadc_var_sensitivity.schema.yml +0 -7
- simtools/schemas/model_parameters/fake_mirror_list.schema.yml +0 -3
- simtools/schemas/model_parameters/flasher_angular_distribution.schema.yml +32 -0
- simtools/schemas/model_parameters/flasher_angular_distribution_width.schema.yml +32 -0
- simtools/schemas/model_parameters/flasher_bunch_size.schema.yml +28 -0
- simtools/schemas/model_parameters/flasher_external_trigger.schema.yml +32 -0
- simtools/schemas/model_parameters/flasher_photons.schema.yml +34 -0
- simtools/schemas/model_parameters/flasher_position.schema.yml +43 -0
- simtools/schemas/model_parameters/flasher_pulse_exp_decay.schema.yml +29 -0
- simtools/schemas/model_parameters/flasher_pulse_offset.schema.yml +35 -0
- simtools/schemas/model_parameters/flasher_pulse_shape.schema.yml +30 -0
- simtools/schemas/model_parameters/flasher_pulse_width.schema.yml +32 -0
- simtools/schemas/model_parameters/flasher_type.schema.yml +28 -0
- simtools/schemas/model_parameters/flasher_var_photons.schema.yml +31 -0
- simtools/schemas/model_parameters/flasher_wavelength.schema.yml +33 -0
- simtools/schemas/model_parameters/flatfielding.schema.yml +0 -7
- simtools/schemas/model_parameters/focal_length.schema.yml +0 -7
- simtools/schemas/model_parameters/focal_surface_parameters.schema.yml +0 -3
- simtools/schemas/model_parameters/focal_surface_ref_radius.schema.yml +0 -3
- simtools/schemas/model_parameters/focus_offset.schema.yml +0 -7
- simtools/schemas/model_parameters/gain_variation.schema.yml +0 -7
- simtools/schemas/model_parameters/geomag_horizontal.schema.yml +2 -7
- simtools/schemas/model_parameters/geomag_rotation.schema.yml +2 -7
- simtools/schemas/model_parameters/geomag_vertical.schema.yml +2 -7
- simtools/schemas/model_parameters/hg_lg_variation.schema.yml +0 -5
- simtools/schemas/model_parameters/iobuf_maximum.schema.yml +0 -7
- simtools/schemas/model_parameters/iobuf_output_maximum.schema.yml +0 -7
- simtools/schemas/model_parameters/laser_events.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_external_trigger.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_photons.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_pulse_exptime.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_pulse_offset.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_pulse_sigtime.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_pulse_twidth.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_var_photons.schema.yml +4 -3
- simtools/schemas/model_parameters/laser_wavelength.schema.yml +4 -3
- simtools/schemas/model_parameters/led_events.schema.yml +4 -3
- simtools/schemas/model_parameters/led_photons.schema.yml +4 -3
- simtools/schemas/model_parameters/led_pulse_offset.schema.yml +4 -3
- simtools/schemas/model_parameters/led_pulse_sigtime.schema.yml +4 -3
- simtools/schemas/model_parameters/led_var_photons.schema.yml +4 -3
- simtools/schemas/model_parameters/lightguide_efficiency_vs_incidence_angle.schema.yml +0 -7
- simtools/schemas/model_parameters/lightguide_efficiency_vs_wavelength.schema.yml +0 -7
- simtools/schemas/model_parameters/min_photoelectrons.schema.yml +0 -7
- simtools/schemas/model_parameters/min_photons.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_align_random_distance.schema.yml +0 -5
- simtools/schemas/model_parameters/mirror_align_random_horizontal.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_align_random_vertical.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_class.schema.yml +2 -9
- simtools/schemas/model_parameters/mirror_degraded_reflection.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_focal_length.schema.yml +0 -5
- simtools/schemas/model_parameters/mirror_list.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_offset.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_reflection_random_angle.schema.yml +0 -7
- simtools/schemas/model_parameters/mirror_reflectivity.schema.yml +0 -7
- simtools/schemas/model_parameters/multiplicity_offset.schema.yml +0 -7
- simtools/schemas/model_parameters/muon_mono_threshold.schema.yml +0 -7
- simtools/schemas/model_parameters/nsb_autoscale_airmass.schema.yml +0 -7
- simtools/schemas/model_parameters/nsb_gain_drop_scale.schema.yml +0 -3
- simtools/schemas/model_parameters/nsb_offaxis.schema.yml +0 -7
- simtools/schemas/model_parameters/nsb_pixel_rate.schema.yml +0 -7
- simtools/schemas/model_parameters/nsb_reference_spectrum.schema.yml +0 -5
- simtools/schemas/model_parameters/nsb_reference_value.schema.yml +0 -5
- simtools/schemas/model_parameters/nsb_scaling_factor.schema.yml +0 -5
- simtools/schemas/model_parameters/nsb_sky_map.schema.yml +0 -5
- simtools/schemas/model_parameters/nsb_spectrum.schema.yml +0 -5
- simtools/schemas/model_parameters/num_gains.schema.yml +0 -7
- simtools/schemas/model_parameters/only_triggered_telescopes.schema.yml +0 -7
- simtools/schemas/model_parameters/optics_properties.schema.yml +0 -7
- simtools/schemas/model_parameters/parabolic_dish.schema.yml +0 -3
- simtools/schemas/model_parameters/pedestal_events.schema.yml +4 -7
- simtools/schemas/model_parameters/photon_delay.schema.yml +0 -7
- simtools/schemas/model_parameters/photons_per_run.schema.yml +4 -4
- simtools/schemas/model_parameters/pixel_cells.schema.yml +0 -3
- simtools/schemas/model_parameters/pixels_parallel.schema.yml +0 -3
- simtools/schemas/model_parameters/pixeltrg_time_step.schema.yml +0 -7
- simtools/schemas/model_parameters/pm_average_gain.schema.yml +0 -5
- simtools/schemas/model_parameters/pm_collection_efficiency.schema.yml +0 -5
- simtools/schemas/model_parameters/pm_gain_index.schema.yml +0 -5
- simtools/schemas/model_parameters/pm_photoelectron_spectrum.schema.yml +0 -7
- simtools/schemas/model_parameters/pm_transit_time.schema.yml +4 -9
- simtools/schemas/model_parameters/pm_voltage_variation.schema.yml +0 -5
- simtools/schemas/model_parameters/primary_mirror_degraded_map.schema.yml +0 -7
- simtools/schemas/model_parameters/primary_mirror_diameter.schema.yml +0 -3
- simtools/schemas/model_parameters/primary_mirror_hole_diameter.schema.yml +0 -3
- simtools/schemas/model_parameters/primary_mirror_incidence_angle.schema.yml +0 -3
- simtools/schemas/model_parameters/primary_mirror_parameters.schema.yml +0 -3
- simtools/schemas/model_parameters/primary_mirror_ref_radius.schema.yml +0 -3
- simtools/schemas/model_parameters/primary_mirror_segmentation.schema.yml +0 -3
- simtools/schemas/model_parameters/qe_variation.schema.yml +0 -7
- simtools/schemas/model_parameters/quantum_efficiency.schema.yml +0 -7
- simtools/schemas/model_parameters/random_focal_length.schema.yml +2 -7
- simtools/schemas/model_parameters/random_generator.schema.yml +0 -7
- simtools/schemas/model_parameters/random_mono_probability.schema.yml +0 -7
- simtools/schemas/model_parameters/reference_point_altitude.schema.yml +0 -5
- simtools/schemas/model_parameters/reference_point_latitude.schema.yml +0 -5
- simtools/schemas/model_parameters/reference_point_longitude.schema.yml +0 -5
- simtools/schemas/model_parameters/reference_point_utm_east.schema.yml +0 -5
- simtools/schemas/model_parameters/reference_point_utm_north.schema.yml +0 -5
- simtools/schemas/model_parameters/sampled_output.schema.yml +0 -7
- simtools/schemas/model_parameters/save_pe_with_amplitude.schema.yml +0 -7
- simtools/schemas/model_parameters/secondary_mirror_baffle.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_degraded_map.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_degraded_reflection.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_diameter.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_hole_diameter.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_incidence_angle.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_parameters.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_ref_radius.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_reflectivity.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_segmentation.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_shadow_diameter.schema.yml +0 -3
- simtools/schemas/model_parameters/secondary_mirror_shadow_offset.schema.yml +0 -3
- simtools/schemas/model_parameters/stars.schema.yml +0 -5
- simtools/schemas/model_parameters/store_photoelectrons.schema.yml +0 -7
- simtools/schemas/model_parameters/tailcut_scale.schema.yml +0 -7
- simtools/schemas/model_parameters/telescope_axis_height.schema.yml +0 -7
- simtools/schemas/model_parameters/telescope_random_angle.schema.yml +0 -7
- simtools/schemas/model_parameters/telescope_random_error.schema.yml +0 -7
- simtools/schemas/model_parameters/telescope_sphere_radius.schema.yml +0 -7
- simtools/schemas/model_parameters/telescope_transmission.schema.yml +0 -7
- simtools/schemas/model_parameters/teltrig_min_sigsum.schema.yml +0 -7
- simtools/schemas/model_parameters/teltrig_min_time.schema.yml +0 -7
- simtools/schemas/model_parameters/transit_time_calib_error.schema.yml +0 -7
- simtools/schemas/model_parameters/transit_time_compensate_error.schema.yml +0 -7
- simtools/schemas/model_parameters/transit_time_compensate_step.schema.yml +0 -7
- simtools/schemas/model_parameters/transit_time_error.schema.yml +0 -7
- simtools/schemas/model_parameters/transit_time_jitter.schema.yml +0 -7
- simtools/schemas/model_parameters/trigger_current_limit.schema.yml +0 -7
- simtools/schemas/model_parameters/trigger_delay_compensation.schema.yml +0 -7
- simtools/schemas/model_parameters/trigger_pixels.schema.yml +0 -7
- simtools/schemas/production_tables.schema.yml +8 -8
- simtools/schemas/simulation_models_info.schema.yml +78 -0
- simtools/simtel/simtel_config_writer.py +88 -14
- simtools/simtel/simulator_array.py +44 -74
- simtools/simtel/simulator_light_emission.py +336 -629
- simtools/simtel/simulator_ray_tracing.py +2 -2
- simtools/simulator.py +46 -18
- simtools/testing/configuration.py +4 -2
- simtools/testing/sim_telarray_metadata.py +4 -4
- simtools/utils/geometry.py +34 -0
- simtools/version.py +111 -0
- simtools/{corsika/corsika_histograms_visualize.py → visualization/plot_corsika_histograms.py} +109 -0
- simtools/visualization/plot_psf.py +775 -0
- simtools/visualization/plot_simtel_events.py +284 -87
- simtools/applications/maintain_simulation_model_add_production_table.py +0 -71
- simtools/model/flasher_model.py +0 -106
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/WHEEL +0 -0
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/licenses/LICENSE +0 -0
- {gammasimtools-0.20.0.dist-info → gammasimtools-0.22.0.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,6 @@ PSF parameter optimisation and fitting routines for mirror alignment and reflect
|
|
|
4
4
|
This module provides functions for loading PSF data, generating random parameter sets,
|
|
5
5
|
running PSF simulations, calculating RMSD, and finding the best-fit parameters for a given
|
|
6
6
|
telescope model.
|
|
7
|
-
|
|
8
7
|
PSF (Point Spread Function) describes how a point source of light is spread out by the
|
|
9
8
|
optical system, and RMSD (Root Mean Squared Deviation) is used as the optimization metric
|
|
10
9
|
to quantify the difference between measured and simulated PSF curves.
|
|
@@ -14,206 +13,89 @@ import logging
|
|
|
14
13
|
from collections import OrderedDict
|
|
15
14
|
|
|
16
15
|
import astropy.units as u
|
|
17
|
-
import matplotlib.pyplot as plt
|
|
18
16
|
import numpy as np
|
|
19
|
-
from
|
|
17
|
+
from astropy.table import Table
|
|
18
|
+
from scipy import stats
|
|
20
19
|
|
|
21
20
|
from simtools.data_model import model_data_writer as writer
|
|
22
|
-
from simtools.model import model_utils
|
|
23
21
|
from simtools.ray_tracing.ray_tracing import RayTracing
|
|
24
22
|
from simtools.utils import general as gen
|
|
25
|
-
from simtools.visualization import
|
|
23
|
+
from simtools.visualization import plot_psf
|
|
24
|
+
from simtools.visualization.plot_psf import DEFAULT_FRACTION, get_psf_diameter_label
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger(__name__)
|
|
28
27
|
|
|
28
|
+
|
|
29
29
|
# Constants
|
|
30
|
-
|
|
30
|
+
RADIUS = "Radius"
|
|
31
31
|
CUMULATIVE_PSF = "Cumulative PSF"
|
|
32
|
+
KS_STATISTIC_NAME = "KS statistic"
|
|
32
33
|
|
|
33
|
-
MRRA_RANGE_DEFAULT = 0.004 # Mirror reflection random angle range
|
|
34
|
-
MRF_RANGE_DEFAULT = 0.1 # Mirror reflection fraction range
|
|
35
|
-
MRRA2_RANGE_DEFAULT = 0.03 # Second mirror reflection random angle range
|
|
36
|
-
MAR_RANGE_DEFAULT = 0.005 # Mirror alignment random range
|
|
37
|
-
MAX_OFFSET_DEFAULT = 4.5 # Maximum off-axis angle in degrees
|
|
38
|
-
OFFSET_STEPS_DEFAULT = 0.1 # Step size for off-axis angle sampling
|
|
39
34
|
|
|
35
|
+
def _create_log_header_and_format_value(title, tel_model, additional_info=None, value=None):
|
|
36
|
+
"""Create log header and format parameter values."""
|
|
37
|
+
if value is not None: # Format value mode
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return "[" + ", ".join([f"{v:.6f}" for v in value]) + "]"
|
|
40
|
+
if isinstance(value, int | float):
|
|
41
|
+
return f"{value:.6f}"
|
|
42
|
+
return str(value)
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Name of the data file with the measured cumulative PSF.
|
|
49
|
-
Expected format:
|
|
50
|
-
Column 0: radial distance in mm
|
|
51
|
-
Column 2: cumulative PSF values
|
|
52
|
-
|
|
53
|
-
Returns
|
|
54
|
-
-------
|
|
55
|
-
numpy.ndarray
|
|
56
|
-
Loaded and processed data with radius in cm and normalized cumulative PSF.
|
|
57
|
-
"""
|
|
58
|
-
d_type = {"names": (RADIUS_CM, CUMULATIVE_PSF), "formats": ("f8", "f8")}
|
|
59
|
-
data = np.loadtxt(data_file, dtype=d_type, usecols=(0, 2))
|
|
60
|
-
data[RADIUS_CM] *= 0.1 # Convert from mm to cm
|
|
61
|
-
data[CUMULATIVE_PSF] /= np.max(np.abs(data[CUMULATIVE_PSF])) # Normalize to max = 1.0
|
|
62
|
-
return data
|
|
44
|
+
# Create header mode
|
|
45
|
+
header_lines = [f"# {title}", f"# Telescope: {tel_model.name}"]
|
|
46
|
+
if additional_info:
|
|
47
|
+
for key, val in additional_info.items():
|
|
48
|
+
header_lines.append(f"# {key}: {val}")
|
|
49
|
+
header_lines.extend(["#" + "=" * 65, ""])
|
|
50
|
+
return "\n".join(header_lines) + "\n"
|
|
63
51
|
|
|
64
52
|
|
|
65
53
|
def calculate_rmsd(data, sim):
|
|
66
|
-
"""Calculate
|
|
54
|
+
"""Calculate RMSD between measured and simulated cumulative PSF curves."""
|
|
67
55
|
return np.sqrt(np.mean((data - sim) ** 2))
|
|
68
56
|
|
|
69
57
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
mirror_align,
|
|
74
|
-
mirror_reflection_fraction=0.15,
|
|
75
|
-
mirror_reflection_2=0.035,
|
|
76
|
-
):
|
|
77
|
-
"""
|
|
78
|
-
Transform and add parameters to the all_parameters list.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
mirror_reflection : float
|
|
83
|
-
The random angle of mirror reflection.
|
|
84
|
-
mirror_align : float
|
|
85
|
-
The random angle for mirror alignment (both horizontal and vertical).
|
|
86
|
-
mirror_reflection_fraction : float, optional
|
|
87
|
-
The fraction of the mirror reflection. Default is 0.15.
|
|
88
|
-
mirror_reflection_2 : float, optional
|
|
89
|
-
A secondary random angle for mirror reflection. Default is 0.035.
|
|
90
|
-
|
|
91
|
-
Returns
|
|
92
|
-
-------
|
|
93
|
-
None
|
|
94
|
-
Updates the all_parameters list in place.
|
|
95
|
-
"""
|
|
96
|
-
pars = {
|
|
97
|
-
"mirror_reflection_random_angle": [
|
|
98
|
-
mirror_reflection,
|
|
99
|
-
mirror_reflection_fraction,
|
|
100
|
-
mirror_reflection_2,
|
|
101
|
-
],
|
|
102
|
-
"mirror_align_random_horizontal": [mirror_align, 28.0, 0.0, 0.0],
|
|
103
|
-
"mirror_align_random_vertical": [mirror_align, 28.0, 0.0, 0.0],
|
|
104
|
-
}
|
|
105
|
-
all_parameters.append(pars)
|
|
58
|
+
def calculate_ks_statistic(data, sim):
|
|
59
|
+
"""Calculate the KS statistic between measured and simulated cumulative PSF curves."""
|
|
60
|
+
return stats.ks_2samp(data, sim)
|
|
106
61
|
|
|
107
62
|
|
|
108
63
|
def get_previous_values(tel_model):
|
|
109
64
|
"""
|
|
110
|
-
Retrieve
|
|
65
|
+
Retrieve current PSF parameter values from the telescope model.
|
|
111
66
|
|
|
112
67
|
Parameters
|
|
113
68
|
----------
|
|
114
69
|
tel_model : TelescopeModel
|
|
115
|
-
Telescope model object.
|
|
70
|
+
Telescope model object containing parameter configurations.
|
|
116
71
|
|
|
117
72
|
Returns
|
|
118
73
|
-------
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
split_par = tel_model.get_parameter_value("mirror_reflection_random_angle")
|
|
125
|
-
mrra_0, mfr_0, mrra2_0 = split_par[0], split_par[1], split_par[2]
|
|
126
|
-
mar_0 = tel_model.get_parameter_value("mirror_align_random_horizontal")[0]
|
|
127
|
-
logger.debug(
|
|
128
|
-
"Previous parameter values:\n"
|
|
129
|
-
f"MRRA = {mrra_0!s}\n"
|
|
130
|
-
f"MRF = {mfr_0!s}\n"
|
|
131
|
-
f"MRRA2 = {mrra2_0!s}\n"
|
|
132
|
-
f"MAR = {mar_0!s}\n"
|
|
133
|
-
)
|
|
134
|
-
return mrra_0, mfr_0, mrra2_0, mar_0
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def generate_random_parameters(
|
|
138
|
-
all_parameters, n_runs, args_dict, mrra_0, mfr_0, mrra2_0, mar_0, tel_model
|
|
139
|
-
):
|
|
140
|
-
"""
|
|
141
|
-
Generate random parameters for tuning.
|
|
142
|
-
|
|
143
|
-
The parameter ranges around the previous values are configurable via module constants.
|
|
144
|
-
|
|
145
|
-
Parameters
|
|
146
|
-
----------
|
|
147
|
-
all_parameters : list
|
|
148
|
-
List to store all parameter sets.
|
|
149
|
-
n_runs : int
|
|
150
|
-
Number of random parameter combinations to test.
|
|
151
|
-
args_dict : dict
|
|
152
|
-
Dictionary containing parsed command-line arguments.
|
|
153
|
-
mrra_0 : float
|
|
154
|
-
Initial value of mirror_reflection_random_angle.
|
|
155
|
-
mfr_0 : float
|
|
156
|
-
Initial value of mirror_reflection_fraction.
|
|
157
|
-
mrra2_0 : float
|
|
158
|
-
Initial value of the second mirror_reflection_random_angle.
|
|
159
|
-
mar_0 : float
|
|
160
|
-
Initial value of mirror_align_random_horizontal/vertical.
|
|
161
|
-
tel_model : TelescopeModel
|
|
162
|
-
Telescope model object to check if it's a dual mirror telescope.
|
|
74
|
+
dict
|
|
75
|
+
Dictionary containing current values of PSF optimization parameters:
|
|
76
|
+
- 'mirror_reflection_random_angle': Random reflection angle parameters
|
|
77
|
+
- 'mirror_align_random_horizontal': Horizontal alignment parameters
|
|
78
|
+
- 'mirror_align_random_vertical': Vertical alignment parameters
|
|
163
79
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
mrf_range = MRF_RANGE_DEFAULT
|
|
176
|
-
mrra2_range = MRRA2_RANGE_DEFAULT
|
|
177
|
-
mar_range = MAR_RANGE_DEFAULT
|
|
178
|
-
rng = np.random.default_rng(seed=args_dict.get("random_seed"))
|
|
179
|
-
mrra = rng.uniform(max(mrra_0 - mrra_range, 0), mrra_0 + mrra_range)
|
|
180
|
-
mrf = rng.uniform(max(mfr_0 - mrf_range, 0), mfr_0 + mrf_range)
|
|
181
|
-
mrra2 = rng.uniform(max(mrra2_0 - mrra2_range, 0), mrra2_0 + mrra2_range)
|
|
182
|
-
|
|
183
|
-
# Set mar to 0 for dual mirror telescopes, otherwise use random value
|
|
184
|
-
if mar_fixed_value is not None:
|
|
185
|
-
mar = mar_fixed_value
|
|
186
|
-
else:
|
|
187
|
-
mar = rng.uniform(max(mar_0 - mar_range, 0), mar_0 + mar_range)
|
|
188
|
-
|
|
189
|
-
add_parameters(all_parameters, mrra, mar, mrf, mrra2)
|
|
80
|
+
return {
|
|
81
|
+
"mirror_reflection_random_angle": tel_model.get_parameter_value(
|
|
82
|
+
"mirror_reflection_random_angle"
|
|
83
|
+
),
|
|
84
|
+
"mirror_align_random_horizontal": tel_model.get_parameter_value(
|
|
85
|
+
"mirror_align_random_horizontal"
|
|
86
|
+
),
|
|
87
|
+
"mirror_align_random_vertical": tel_model.get_parameter_value(
|
|
88
|
+
"mirror_align_random_vertical"
|
|
89
|
+
),
|
|
90
|
+
}
|
|
190
91
|
|
|
191
92
|
|
|
192
93
|
def _run_ray_tracing_simulation(tel_model, site_model, args_dict, pars):
|
|
193
|
-
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Parameters
|
|
197
|
-
----------
|
|
198
|
-
tel_model : TelescopeModel
|
|
199
|
-
Telescope model object.
|
|
200
|
-
site_model : SiteModel
|
|
201
|
-
Site model object.
|
|
202
|
-
args_dict : dict
|
|
203
|
-
Dictionary containing parsed command-line arguments.
|
|
204
|
-
pars : dict
|
|
205
|
-
Parameter set dictionary.
|
|
206
|
-
|
|
207
|
-
Returns
|
|
208
|
-
-------
|
|
209
|
-
tuple
|
|
210
|
-
(d80, simulated_data) - D80 value and simulated data from ray tracing.
|
|
211
|
-
"""
|
|
212
|
-
if pars is not None:
|
|
213
|
-
tel_model.change_multiple_parameters(**pars)
|
|
214
|
-
else:
|
|
94
|
+
"""Run a ray tracing simulation with the given telescope parameters."""
|
|
95
|
+
if pars is None:
|
|
215
96
|
raise ValueError("No best parameters found")
|
|
216
97
|
|
|
98
|
+
tel_model.change_multiple_parameters(**pars)
|
|
217
99
|
ray = RayTracing(
|
|
218
100
|
telescope_model=tel_model,
|
|
219
101
|
site_model=site_model,
|
|
@@ -225,568 +107,1227 @@ def _run_ray_tracing_simulation(tel_model, site_model, args_dict, pars):
|
|
|
225
107
|
ray.simulate(test=args_dict.get("test", False), force=True)
|
|
226
108
|
ray.analyze(force=True, use_rx=False)
|
|
227
109
|
im = ray.images()[0]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return d80, im
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _create_psf_simulation_plot(data_to_plot, pars, d80, rmsd, is_best, pdf_pages):
|
|
234
|
-
"""
|
|
235
|
-
Create a plot for PSF simulation results.
|
|
236
|
-
|
|
237
|
-
Parameters
|
|
238
|
-
----------
|
|
239
|
-
data_to_plot : dict
|
|
240
|
-
Data dictionary for plotting.
|
|
241
|
-
pars : dict
|
|
242
|
-
Parameter set dictionary.
|
|
243
|
-
d80 : float
|
|
244
|
-
D80 value.
|
|
245
|
-
rmsd : float
|
|
246
|
-
RMSD value.
|
|
247
|
-
is_best : bool
|
|
248
|
-
Whether this is the best parameter set.
|
|
249
|
-
pdf_pages : PdfPages
|
|
250
|
-
PDF pages object for saving plots.
|
|
251
|
-
"""
|
|
252
|
-
fig = visualize.plot_1d(
|
|
253
|
-
data_to_plot,
|
|
254
|
-
plot_difference=True,
|
|
255
|
-
no_markers=True,
|
|
256
|
-
)
|
|
257
|
-
ax = fig.get_axes()[0]
|
|
258
|
-
ax.set_ylim(0, 1.05)
|
|
259
|
-
ax.set_ylabel(CUMULATIVE_PSF)
|
|
260
|
-
|
|
261
|
-
title_prefix = "* " if is_best else ""
|
|
262
|
-
ax.set_title(
|
|
263
|
-
f"{title_prefix}refl_rnd = "
|
|
264
|
-
f"{pars['mirror_reflection_random_angle'][0]:.5f}, "
|
|
265
|
-
f"{pars['mirror_reflection_random_angle'][1]:.5f}, "
|
|
266
|
-
f"{pars['mirror_reflection_random_angle'][2]:.5f}\n"
|
|
267
|
-
f"align_rnd = {pars['mirror_align_random_vertical'][0]:.5f}, "
|
|
268
|
-
f"{pars['mirror_align_random_vertical'][1]:.5f}, "
|
|
269
|
-
f"{pars['mirror_align_random_vertical'][2]:.5f}, "
|
|
270
|
-
f"{pars['mirror_align_random_vertical'][3]:.5f}"
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
d80_color = "red" if is_best else "black"
|
|
274
|
-
d80_weight = "bold" if is_best else "normal"
|
|
275
|
-
d80_text = f"D80 = {d80:.5f} cm"
|
|
276
|
-
|
|
277
|
-
ax.text(
|
|
278
|
-
0.5,
|
|
279
|
-
0.3,
|
|
280
|
-
f"{d80_text}\nRMSD = {rmsd:.4f}",
|
|
281
|
-
verticalalignment="center",
|
|
282
|
-
horizontalalignment="left",
|
|
283
|
-
transform=ax.transAxes,
|
|
284
|
-
color=d80_color,
|
|
285
|
-
weight=d80_weight,
|
|
286
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "yellow", "alpha": 0.7}
|
|
287
|
-
if is_best
|
|
288
|
-
else None,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
if is_best:
|
|
292
|
-
fig.text(
|
|
293
|
-
0.02,
|
|
294
|
-
0.02,
|
|
295
|
-
"* Best parameter set (lowest RMSD)",
|
|
296
|
-
fontsize=8,
|
|
297
|
-
style="italic",
|
|
298
|
-
color="red",
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
pdf_pages.savefig(fig, bbox_inches="tight")
|
|
302
|
-
plt.clf()
|
|
110
|
+
fraction = args_dict.get("fraction", DEFAULT_FRACTION)
|
|
111
|
+
return im.get_psf(fraction=fraction), im
|
|
303
112
|
|
|
304
113
|
|
|
305
114
|
def run_psf_simulation(
|
|
306
115
|
tel_model,
|
|
307
|
-
|
|
116
|
+
site,
|
|
308
117
|
args_dict,
|
|
309
118
|
pars,
|
|
310
119
|
data_to_plot,
|
|
311
120
|
radius,
|
|
312
121
|
pdf_pages=None,
|
|
313
122
|
is_best=False,
|
|
314
|
-
|
|
123
|
+
use_ks_statistic=False,
|
|
315
124
|
):
|
|
316
125
|
"""
|
|
317
|
-
Run
|
|
126
|
+
Run PSF simulation for given parameters and calculate optimization metric.
|
|
318
127
|
|
|
319
128
|
Parameters
|
|
320
129
|
----------
|
|
321
130
|
tel_model : TelescopeModel
|
|
322
|
-
Telescope model object.
|
|
323
|
-
|
|
324
|
-
Site model object.
|
|
131
|
+
Telescope model object to be configured with the test parameters.
|
|
132
|
+
site : Site
|
|
133
|
+
Site model object with environmental conditions.
|
|
325
134
|
args_dict : dict
|
|
326
|
-
Dictionary containing
|
|
135
|
+
Dictionary containing simulation configuration arguments.
|
|
327
136
|
pars : dict
|
|
328
|
-
|
|
137
|
+
Dictionary of parameter values to test in the simulation.
|
|
329
138
|
data_to_plot : dict
|
|
330
|
-
|
|
139
|
+
Dictionary containing measured PSF data under "measured" key.
|
|
331
140
|
radius : array-like
|
|
332
|
-
Radius
|
|
141
|
+
Radius values in cm for PSF evaluation.
|
|
333
142
|
pdf_pages : PdfPages, optional
|
|
334
|
-
PDF pages object for
|
|
143
|
+
PDF pages object for saving plots (default: None).
|
|
335
144
|
is_best : bool, optional
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
If True,
|
|
145
|
+
Flag indicating if this is the best parameter set (default: False).
|
|
146
|
+
use_ks_statistic : bool, optional
|
|
147
|
+
If True, use KS statistic as metric; if False, use RMSD (default: False).
|
|
339
148
|
|
|
340
149
|
Returns
|
|
341
150
|
-------
|
|
342
|
-
tuple
|
|
343
|
-
|
|
344
|
-
|
|
151
|
+
tuple of (float, float, float or None, array)
|
|
152
|
+
- psf_diameter: PSF containment diameter of the simulated PSF in cm
|
|
153
|
+
- metric: RMSD or KS statistic value
|
|
154
|
+
- p_value: p-value from KS test (None if using RMSD)
|
|
155
|
+
- simulated_data: Structured array with simulated cumulative PSF data
|
|
345
156
|
"""
|
|
346
|
-
|
|
157
|
+
psf_diameter, im = _run_ray_tracing_simulation(tel_model, site, args_dict, pars)
|
|
347
158
|
|
|
348
159
|
if radius is None:
|
|
349
160
|
raise ValueError("Radius data is not available.")
|
|
350
161
|
|
|
351
162
|
simulated_data = im.get_cumulative_data(radius * u.cm)
|
|
352
|
-
|
|
163
|
+
|
|
164
|
+
if use_ks_statistic:
|
|
165
|
+
ks_statistic, p_value = calculate_ks_statistic(
|
|
166
|
+
data_to_plot["measured"][CUMULATIVE_PSF], simulated_data[CUMULATIVE_PSF]
|
|
167
|
+
)
|
|
168
|
+
metric = ks_statistic
|
|
169
|
+
else:
|
|
170
|
+
metric = calculate_rmsd(
|
|
171
|
+
data_to_plot["measured"][CUMULATIVE_PSF], simulated_data[CUMULATIVE_PSF]
|
|
172
|
+
)
|
|
173
|
+
p_value = None
|
|
353
174
|
|
|
354
175
|
# Handle plotting if requested
|
|
355
176
|
if pdf_pages is not None and args_dict.get("plot_all", False):
|
|
356
177
|
data_to_plot["simulated"] = simulated_data
|
|
357
|
-
|
|
178
|
+
plot_psf.create_psf_parameter_plot(
|
|
179
|
+
data_to_plot,
|
|
180
|
+
pars,
|
|
181
|
+
psf_diameter,
|
|
182
|
+
metric,
|
|
183
|
+
is_best,
|
|
184
|
+
pdf_pages,
|
|
185
|
+
fraction=args_dict.get("fraction", DEFAULT_FRACTION),
|
|
186
|
+
p_value=p_value,
|
|
187
|
+
use_ks_statistic=use_ks_statistic,
|
|
188
|
+
)
|
|
358
189
|
del data_to_plot["simulated"]
|
|
359
190
|
|
|
360
|
-
return
|
|
191
|
+
return psf_diameter, metric, p_value, simulated_data
|
|
361
192
|
|
|
362
193
|
|
|
363
194
|
def load_and_process_data(args_dict):
|
|
364
195
|
"""
|
|
365
|
-
Load and process
|
|
196
|
+
Load and process PSF measurement data from ECSV file.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
args_dict : dict
|
|
201
|
+
Dictionary containing command-line arguments with 'data' and 'model_path' keys.
|
|
366
202
|
|
|
367
203
|
Returns
|
|
368
204
|
-------
|
|
369
|
-
|
|
370
|
-
|
|
205
|
+
tuple of (OrderedDict, array)
|
|
206
|
+
- data_dict: OrderedDict with "measured" key containing structured array
|
|
207
|
+
of radius and cumulative PSF data
|
|
208
|
+
- radius: Array of radius values in cm
|
|
209
|
+
|
|
210
|
+
Raises
|
|
211
|
+
------
|
|
212
|
+
FileNotFoundError
|
|
213
|
+
If no data file is specified in args_dict.
|
|
371
214
|
"""
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
215
|
+
if args_dict["data"] is None:
|
|
216
|
+
raise FileNotFoundError("No data file specified for PSF optimization.")
|
|
217
|
+
|
|
218
|
+
data_file = gen.find_file(args_dict["data"], args_dict["model_path"])
|
|
219
|
+
table = Table.read(data_file, format="ascii.ecsv")
|
|
220
|
+
|
|
221
|
+
radius_column = next((col for col in table.colnames if "radius" in col.lower()), None)
|
|
222
|
+
integral_psf_column = next((col for col in table.colnames if "integral" in col.lower()), None)
|
|
223
|
+
|
|
224
|
+
# Create structured array with converted data
|
|
225
|
+
d_type = {"names": (RADIUS, CUMULATIVE_PSF), "formats": ("f8", "f8")}
|
|
226
|
+
data = np.zeros(len(table), dtype=d_type)
|
|
379
227
|
|
|
228
|
+
data[RADIUS] = table[radius_column].to(u.cm).value
|
|
229
|
+
data[CUMULATIVE_PSF] = table[integral_psf_column]
|
|
230
|
+
data[CUMULATIVE_PSF] /= np.max(np.abs(data[CUMULATIVE_PSF])) # Normalize to max = 1.0
|
|
231
|
+
|
|
232
|
+
return OrderedDict([("measured", data)]), data[RADIUS]
|
|
380
233
|
|
|
381
|
-
|
|
234
|
+
|
|
235
|
+
def write_tested_parameters_to_file(
|
|
236
|
+
results, best_pars, best_psf_diameter, output_dir, tel_model, fraction=DEFAULT_FRACTION
|
|
237
|
+
):
|
|
382
238
|
"""
|
|
383
|
-
|
|
239
|
+
Write optimization results and tested parameters to a log file.
|
|
384
240
|
|
|
385
241
|
Parameters
|
|
386
242
|
----------
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
243
|
+
results : list
|
|
244
|
+
List of tuples containing (parameters, ks_statistic, p_value, psf_diameter, simulated_data)
|
|
245
|
+
for each tested parameter set.
|
|
246
|
+
best_pars : dict
|
|
247
|
+
Dictionary containing the best parameter values found.
|
|
248
|
+
best_psf_diameter : float
|
|
249
|
+
PSF containment diameter in cm for the best parameter set.
|
|
250
|
+
output_dir : Path
|
|
251
|
+
Directory where the log file will be written.
|
|
252
|
+
tel_model : TelescopeModel
|
|
253
|
+
Telescope model object for naming the output file.
|
|
254
|
+
fraction : float, optional
|
|
255
|
+
PSF containment fraction for labeling (default: 0.8).
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
Path
|
|
260
|
+
Path to the created log file.
|
|
401
261
|
"""
|
|
402
|
-
|
|
403
|
-
|
|
262
|
+
param_file = output_dir.joinpath(f"psf_optimization_{tel_model.name}.log")
|
|
263
|
+
psf_label = get_psf_diameter_label(fraction)
|
|
404
264
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
ax.set_ylabel(CUMULATIVE_PSF)
|
|
413
|
-
|
|
414
|
-
title_prefix = "* " if is_best else ""
|
|
415
|
-
|
|
416
|
-
ax.set_title(
|
|
417
|
-
f"{title_prefix}reflection = "
|
|
418
|
-
f"{pars['mirror_reflection_random_angle'][0]:.5f}, "
|
|
419
|
-
f"{pars['mirror_reflection_random_angle'][1]:.5f}, "
|
|
420
|
-
f"{pars['mirror_reflection_random_angle'][2]:.5f}\n"
|
|
421
|
-
f"align_vertical = {pars['mirror_align_random_vertical'][0]:.5f}, "
|
|
422
|
-
f"{pars['mirror_align_random_vertical'][1]:.5f}, "
|
|
423
|
-
f"{pars['mirror_align_random_vertical'][2]:.5f}, "
|
|
424
|
-
f"{pars['mirror_align_random_vertical'][3]:.5f}\n"
|
|
425
|
-
f"align_horizontal = {pars['mirror_align_random_horizontal'][0]:.5f}, "
|
|
426
|
-
f"{pars['mirror_align_random_horizontal'][1]:.5f}, "
|
|
427
|
-
f"{pars['mirror_align_random_horizontal'][2]:.5f}, "
|
|
428
|
-
f"{pars['mirror_align_random_horizontal'][3]:.5f}"
|
|
429
|
-
)
|
|
265
|
+
with open(param_file, "w", encoding="utf-8") as f:
|
|
266
|
+
header = _create_log_header_and_format_value(
|
|
267
|
+
"PSF Parameter Optimization Log",
|
|
268
|
+
tel_model,
|
|
269
|
+
{"Total parameter sets tested": len(results)},
|
|
270
|
+
)
|
|
271
|
+
f.write(header)
|
|
430
272
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
color=d80_color,
|
|
442
|
-
weight=d80_weight,
|
|
443
|
-
bbox={"boxstyle": "round,pad=0.3", "facecolor": "yellow", "alpha": 0.7}
|
|
444
|
-
if is_best
|
|
445
|
-
else None,
|
|
446
|
-
)
|
|
273
|
+
f.write("PARAMETER TESTING RESULTS:\n")
|
|
274
|
+
for i, (pars, ks_statistic, p_value, psf_diameter, _) in enumerate(results):
|
|
275
|
+
status = "BEST" if pars is best_pars else "TESTED"
|
|
276
|
+
f.write(
|
|
277
|
+
f"[{status}] Set {i + 1:03d}: KS_stat={ks_statistic:.5f}, "
|
|
278
|
+
f"p_value={p_value:.5f}, {psf_label}={psf_diameter:.5f} cm\n"
|
|
279
|
+
)
|
|
280
|
+
for par, value in pars.items():
|
|
281
|
+
f.write(f" {par}: {value}\n")
|
|
282
|
+
f.write("\n")
|
|
447
283
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
color="red",
|
|
456
|
-
)
|
|
284
|
+
f.write("OPTIMIZATION SUMMARY:\n")
|
|
285
|
+
f.write(f"Best KS statistic: {min(result[1] for result in results):.5f}\n")
|
|
286
|
+
f.write(f"Best {psf_label}: {best_psf_diameter:.5f} cm\n")
|
|
287
|
+
f.write("\nOPTIMIZED PARAMETERS:\n")
|
|
288
|
+
for par, value in best_pars.items():
|
|
289
|
+
f.write(f"{par}: {value}\n")
|
|
290
|
+
return param_file
|
|
457
291
|
|
|
458
|
-
pdf_pages.savefig(fig, bbox_inches="tight")
|
|
459
|
-
plt.clf()
|
|
460
292
|
|
|
461
|
-
|
|
462
|
-
|
|
293
|
+
def _add_units_to_psf_parameters(best_pars):
|
|
294
|
+
"""Add astropy units to PSF parameters based on their schemas."""
|
|
295
|
+
psf_pars_with_units = {}
|
|
296
|
+
for param_name, param_values in best_pars.items():
|
|
297
|
+
if param_name == "mirror_reflection_random_angle":
|
|
298
|
+
psf_pars_with_units[param_name] = [
|
|
299
|
+
param_values[0] * u.deg,
|
|
300
|
+
param_values[1] * u.dimensionless_unscaled,
|
|
301
|
+
param_values[2] * u.deg,
|
|
302
|
+
]
|
|
303
|
+
elif param_name in ["mirror_align_random_horizontal", "mirror_align_random_vertical"]:
|
|
304
|
+
psf_pars_with_units[param_name] = [
|
|
305
|
+
param_values[0] * u.deg,
|
|
306
|
+
param_values[1] * u.deg,
|
|
307
|
+
param_values[2] * u.dimensionless_unscaled,
|
|
308
|
+
param_values[3] * u.dimensionless_unscaled,
|
|
309
|
+
]
|
|
310
|
+
else:
|
|
311
|
+
psf_pars_with_units[param_name] = param_values
|
|
312
|
+
return psf_pars_with_units
|
|
463
313
|
|
|
464
314
|
|
|
465
|
-
def
|
|
315
|
+
def export_psf_parameters(best_pars, telescope, parameter_version, output_dir):
|
|
466
316
|
"""
|
|
467
|
-
|
|
317
|
+
Export optimized PSF parameters as simulation model parameter files.
|
|
468
318
|
|
|
469
319
|
Parameters
|
|
470
320
|
----------
|
|
471
|
-
results : list
|
|
472
|
-
List of (pars, rmsd, d80, simulated_data) tuples
|
|
473
321
|
best_pars : dict
|
|
474
|
-
|
|
322
|
+
Dictionary containing the optimized parameter values.
|
|
323
|
+
telescope : str
|
|
324
|
+
Telescope name for the parameter files.
|
|
325
|
+
parameter_version : str
|
|
326
|
+
Version string for the parameter files.
|
|
327
|
+
output_dir : Path
|
|
328
|
+
Base directory for parameter file output.
|
|
329
|
+
|
|
330
|
+
Notes
|
|
331
|
+
-----
|
|
332
|
+
Creates individual JSON files for each optimized parameter with
|
|
333
|
+
units. Files are saved in the format:
|
|
334
|
+
{output_dir}/{telescope}/{parameter_name}-{parameter_version}.json
|
|
335
|
+
|
|
336
|
+
Raises
|
|
337
|
+
------
|
|
338
|
+
ValueError, KeyError, OSError
|
|
339
|
+
If parameter export fails due to invalid values, missing keys, or file I/O errors.
|
|
340
|
+
"""
|
|
341
|
+
try:
|
|
342
|
+
psf_pars_with_units = _add_units_to_psf_parameters(best_pars)
|
|
343
|
+
parameter_output_path = output_dir.parent / telescope
|
|
344
|
+
for parameter_name, parameter_value in psf_pars_with_units.items():
|
|
345
|
+
writer.ModelDataWriter.dump_model_parameter(
|
|
346
|
+
parameter_name=parameter_name,
|
|
347
|
+
value=parameter_value,
|
|
348
|
+
instrument=telescope,
|
|
349
|
+
parameter_version=parameter_version,
|
|
350
|
+
output_file=f"{parameter_name}-{parameter_version}.json",
|
|
351
|
+
output_path=parameter_output_path,
|
|
352
|
+
)
|
|
353
|
+
logger.info(f"simulation model parameter files exported to {output_dir}")
|
|
354
|
+
|
|
355
|
+
except (ValueError, KeyError, OSError) as e:
|
|
356
|
+
logger.error(f"Error exporting simulation parameters: {e}")
|
|
357
|
+
|
|
358
|
+
|
|
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.
|
|
475
388
|
data_to_plot : dict
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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.
|
|
479
411
|
"""
|
|
480
|
-
|
|
412
|
+
param_gradients = []
|
|
413
|
+
values_list = param_values if isinstance(param_values, list) else [param_values]
|
|
481
414
|
|
|
482
|
-
for i,
|
|
483
|
-
|
|
484
|
-
|
|
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
|
+
}
|
|
485
419
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
489
442
|
|
|
490
443
|
|
|
491
|
-
def
|
|
492
|
-
|
|
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,
|
|
493
454
|
):
|
|
494
455
|
"""
|
|
495
|
-
|
|
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.
|
|
496
517
|
|
|
497
|
-
|
|
498
|
-
|
|
518
|
+
Returns
|
|
519
|
+
-------
|
|
520
|
+
dict
|
|
521
|
+
Dictionary of updated parameter values after applying the gradient step.
|
|
499
522
|
"""
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
|
504
534
|
|
|
505
|
-
|
|
535
|
+
return new_params
|
|
506
536
|
|
|
507
|
-
|
|
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):
|
|
508
597
|
try:
|
|
509
|
-
|
|
510
|
-
d80, rmsd, simulated_data = run_psf_simulation(
|
|
598
|
+
gradients = calculate_gradient(
|
|
511
599
|
tel_model,
|
|
512
600
|
site_model,
|
|
513
601
|
args_dict,
|
|
514
|
-
|
|
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,
|
|
515
615
|
data_to_plot,
|
|
516
616
|
radius,
|
|
517
|
-
return_simulated_data=True,
|
|
518
617
|
pdf_pages=None,
|
|
618
|
+
is_best=False,
|
|
619
|
+
use_ks_statistic=False,
|
|
519
620
|
)
|
|
520
|
-
|
|
521
|
-
|
|
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}")
|
|
522
644
|
continue
|
|
523
645
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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"]
|
|
529
676
|
|
|
530
|
-
logger.info(f"Best RMSD found: {best_rmsd:.5f}")
|
|
531
677
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
+
)
|
|
535
707
|
|
|
536
|
-
|
|
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")
|
|
537
724
|
|
|
538
725
|
|
|
539
|
-
def
|
|
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
|
+
):
|
|
540
736
|
"""
|
|
541
|
-
|
|
737
|
+
Run gradient descent optimization to minimize PSF fitting metric.
|
|
542
738
|
|
|
543
739
|
Parameters
|
|
544
740
|
----------
|
|
545
741
|
tel_model : TelescopeModel
|
|
546
|
-
Telescope model object.
|
|
742
|
+
Telescope model object to be optimized.
|
|
547
743
|
site_model : SiteModel
|
|
548
|
-
Site model object.
|
|
744
|
+
Site model object with environmental conditions.
|
|
549
745
|
args_dict : dict
|
|
550
|
-
Dictionary containing
|
|
551
|
-
|
|
552
|
-
|
|
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.
|
|
553
755
|
output_dir : Path
|
|
554
|
-
|
|
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.
|
|
555
767
|
"""
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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,
|
|
568
787
|
)
|
|
569
788
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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,
|
|
577
802
|
)
|
|
578
803
|
|
|
579
|
-
logger.info(f"
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
582
885
|
|
|
583
|
-
for key in ["d80_cm", "d80_deg"]:
|
|
584
|
-
plt.figure(figsize=(10, 6), tight_layout=True)
|
|
585
886
|
|
|
586
|
-
|
|
887
|
+
def _write_log_interpretation(f, use_ks_statistic):
|
|
888
|
+
"""Write interpretation section for the log file."""
|
|
889
|
+
if use_ks_statistic:
|
|
890
|
+
f.write(
|
|
891
|
+
"P-VALUE INTERPRETATION:\n p > 0.05: Distributions are statistically similar "
|
|
892
|
+
"(good fit)\n"
|
|
893
|
+
" p < 0.05: Distributions are significantly different (poor fit)\n"
|
|
894
|
+
" p < 0.01: Very significant difference (very poor fit)\n\n"
|
|
895
|
+
)
|
|
896
|
+
else:
|
|
897
|
+
f.write(
|
|
898
|
+
"RMSD INTERPRETATION:\n Lower RMSD values indicate better agreement between "
|
|
899
|
+
"measured and simulated PSF curves\n\n"
|
|
900
|
+
)
|
|
587
901
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
902
|
+
|
|
903
|
+
def _write_iteration_entry(
|
|
904
|
+
f,
|
|
905
|
+
iteration,
|
|
906
|
+
pars,
|
|
907
|
+
metric,
|
|
908
|
+
p_value,
|
|
909
|
+
psf_diameter,
|
|
910
|
+
use_ks_statistic,
|
|
911
|
+
metric_name,
|
|
912
|
+
total_iterations,
|
|
913
|
+
fraction=DEFAULT_FRACTION,
|
|
914
|
+
):
|
|
915
|
+
"""Write a single iteration entry."""
|
|
916
|
+
status = "FINAL" if iteration == total_iterations - 1 else f"ITER-{iteration:02d}"
|
|
917
|
+
|
|
918
|
+
if use_ks_statistic and p_value is not None:
|
|
919
|
+
significance = plot_psf.get_significance_label(p_value)
|
|
920
|
+
label = get_psf_diameter_label(fraction)
|
|
921
|
+
f.write(
|
|
922
|
+
f"[{status}] Iteration {iteration}: KS_stat={metric:.6f}, "
|
|
923
|
+
f"p_value={p_value:.6f} ({significance}), {label}={psf_diameter:.6f} cm\n"
|
|
924
|
+
)
|
|
925
|
+
else:
|
|
926
|
+
label = get_psf_diameter_label(fraction)
|
|
927
|
+
f.write(
|
|
928
|
+
f"[{status}] Iteration {iteration}: {metric_name}={metric:.6f}, "
|
|
929
|
+
f"{label}={psf_diameter:.6f} cm\n"
|
|
596
930
|
)
|
|
597
|
-
plt.xlabel("Off-axis Angle (degrees)")
|
|
598
|
-
plt.ylabel("D80 (cm)" if key == "d80_cm" else "D80 (degrees)")
|
|
599
|
-
plt.ylim(bottom=0)
|
|
600
|
-
plt.xticks(rotation=45)
|
|
601
|
-
plt.xlim(0, max_offset)
|
|
602
|
-
plt.grid(True, alpha=0.3)
|
|
603
931
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
932
|
+
for par, value in pars.items():
|
|
933
|
+
f.write(f" {par}: {_create_log_header_and_format_value(None, None, None, value)}\n")
|
|
934
|
+
f.write("\n")
|
|
607
935
|
|
|
608
|
-
|
|
936
|
+
|
|
937
|
+
def _write_optimization_summary(
|
|
938
|
+
f, gd_results, best_pars, best_psf_diameter, metric_name, fraction=DEFAULT_FRACTION
|
|
939
|
+
):
|
|
940
|
+
"""Write optimization summary section."""
|
|
941
|
+
f.write("OPTIMIZATION SUMMARY:\n")
|
|
942
|
+
best_metric_from_results = min(metric for _, metric, _, _, _ in gd_results)
|
|
943
|
+
f.write(f"Best {metric_name.lower()}: {best_metric_from_results:.6f}\n")
|
|
944
|
+
|
|
945
|
+
label = get_psf_diameter_label(fraction)
|
|
946
|
+
f.write(
|
|
947
|
+
f"Best {label}: {best_psf_diameter:.6f} cm\n"
|
|
948
|
+
if best_psf_diameter is not None
|
|
949
|
+
else f"Best {label}: N/A\n"
|
|
950
|
+
)
|
|
951
|
+
f.write(f"Total iterations: {len(gd_results)}\n\nFINAL OPTIMIZED PARAMETERS:\n")
|
|
952
|
+
for par, value in best_pars.items():
|
|
953
|
+
f.write(f"{par}: {_create_log_header_and_format_value(None, None, None, value)}\n")
|
|
609
954
|
|
|
610
955
|
|
|
611
|
-
def
|
|
956
|
+
def write_gradient_descent_log(
|
|
957
|
+
gd_results,
|
|
958
|
+
best_pars,
|
|
959
|
+
best_psf_diameter,
|
|
960
|
+
output_dir,
|
|
961
|
+
tel_model,
|
|
962
|
+
use_ks_statistic=False,
|
|
963
|
+
fraction=DEFAULT_FRACTION,
|
|
964
|
+
):
|
|
612
965
|
"""
|
|
613
|
-
Write
|
|
966
|
+
Write gradient descent optimization progression to a log file.
|
|
614
967
|
|
|
615
968
|
Parameters
|
|
616
969
|
----------
|
|
617
|
-
|
|
618
|
-
List of (
|
|
970
|
+
gd_results : list
|
|
971
|
+
List of tuples containing (params, metric, p_value, psf_diameter, simulated_data)
|
|
972
|
+
for each optimization iteration.
|
|
619
973
|
best_pars : dict
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
974
|
+
Dictionary containing the best parameter values found.
|
|
975
|
+
best_psf_diameter : float
|
|
976
|
+
PSF containment diameter in cm for the best parameter set.
|
|
623
977
|
output_dir : Path
|
|
624
|
-
|
|
978
|
+
Directory where the log file will be written.
|
|
625
979
|
tel_model : TelescopeModel
|
|
626
|
-
Telescope model object for
|
|
980
|
+
Telescope model object for naming the output file.
|
|
981
|
+
use_ks_statistic : bool, optional
|
|
982
|
+
If True, log KS statistic values; if False, log RMSD values (default: False).
|
|
983
|
+
fraction : float, optional
|
|
984
|
+
PSF containment fraction for labeling (default: 0.8).
|
|
985
|
+
|
|
986
|
+
Returns
|
|
987
|
+
-------
|
|
988
|
+
Path
|
|
989
|
+
Path to the created log file.
|
|
627
990
|
"""
|
|
628
|
-
|
|
991
|
+
metric_name = "KS Statistic" if use_ks_statistic else "RMSD"
|
|
992
|
+
file_suffix = "ks" if use_ks_statistic else "rmsd"
|
|
993
|
+
param_file = output_dir.joinpath(f"psf_gradient_descent_{file_suffix}_{tel_model.name}.log")
|
|
994
|
+
|
|
629
995
|
with open(param_file, "w", encoding="utf-8") as f:
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
996
|
+
header = _create_log_header_and_format_value(
|
|
997
|
+
f"PSF Parameter Optimization - Gradient Descent Progression ({metric_name})",
|
|
998
|
+
tel_model,
|
|
999
|
+
{"Total iterations": len(gd_results)},
|
|
1000
|
+
)
|
|
1001
|
+
f.write(header)
|
|
634
1002
|
|
|
635
|
-
f.write(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1003
|
+
f.write(
|
|
1004
|
+
"GRADIENT DESCENT PROGRESSION:\n(Each entry shows the parameters chosen "
|
|
1005
|
+
"at each iteration)\n\n"
|
|
1006
|
+
)
|
|
1007
|
+
_write_log_interpretation(f, use_ks_statistic)
|
|
1008
|
+
|
|
1009
|
+
for iteration, (pars, metric, p_value, psf_diameter, _) in enumerate(gd_results):
|
|
1010
|
+
_write_iteration_entry(
|
|
1011
|
+
f,
|
|
1012
|
+
iteration,
|
|
1013
|
+
pars,
|
|
1014
|
+
metric,
|
|
1015
|
+
p_value,
|
|
1016
|
+
psf_diameter,
|
|
1017
|
+
use_ks_statistic,
|
|
1018
|
+
metric_name,
|
|
1019
|
+
len(gd_results),
|
|
1020
|
+
fraction,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
_write_optimization_summary(
|
|
1024
|
+
f, gd_results, best_pars, best_psf_diameter, metric_name, fraction
|
|
1025
|
+
)
|
|
643
1026
|
|
|
644
|
-
f.write("OPTIMIZATION SUMMARY:\n")
|
|
645
|
-
f.write(f"Best RMSD: {min(result[1] for result in results):.5f}\n")
|
|
646
|
-
f.write(f"Best D80: {best_d80:.5f} cm\n")
|
|
647
|
-
f.write("\nOPTIMIZED PARAMETERS:\n")
|
|
648
|
-
for par, value in best_pars.items():
|
|
649
|
-
f.write(f"{par}: {value}\n")
|
|
650
1027
|
return param_file
|
|
651
1028
|
|
|
652
1029
|
|
|
653
|
-
def
|
|
1030
|
+
def analyze_monte_carlo_error(
|
|
1031
|
+
tel_model, site_model, args_dict, data_to_plot, radius, n_simulations=500
|
|
1032
|
+
):
|
|
654
1033
|
"""
|
|
655
|
-
|
|
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.
|
|
656
1040
|
|
|
657
1041
|
Parameters
|
|
658
1042
|
----------
|
|
659
|
-
|
|
660
|
-
|
|
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).
|
|
661
1055
|
|
|
662
1056
|
Returns
|
|
663
1057
|
-------
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
666
1068
|
"""
|
|
667
|
-
|
|
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, []
|
|
668
1072
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
param_values[0] * u.deg,
|
|
673
|
-
param_values[1] * u.dimensionless_unscaled,
|
|
674
|
-
param_values[2] * u.deg,
|
|
675
|
-
]
|
|
676
|
-
elif param_name in ["mirror_align_random_horizontal", "mirror_align_random_vertical"]:
|
|
677
|
-
psf_pars_with_units[param_name] = [
|
|
678
|
-
param_values[0] * u.deg,
|
|
679
|
-
param_values[1] * u.deg,
|
|
680
|
-
param_values[2] * u.dimensionless_unscaled,
|
|
681
|
-
param_values[3] * u.dimensionless_unscaled,
|
|
682
|
-
]
|
|
683
|
-
else:
|
|
684
|
-
psf_pars_with_units[param_name] = param_values
|
|
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}")
|
|
685
1076
|
|
|
686
|
-
|
|
1077
|
+
use_ks_statistic = args_dict.get("ks_statistic", False)
|
|
1078
|
+
metric_values, p_values, psf_diameter_values = [], [], []
|
|
687
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}")
|
|
688
1096
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1097
|
+
if not metric_values:
|
|
1098
|
+
logger.error("All Monte Carlo simulations failed.")
|
|
1099
|
+
return None, None, [], None, None, []
|
|
692
1100
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
Telescope model object
|
|
699
|
-
parameter_version : str
|
|
700
|
-
Parameter version string
|
|
701
|
-
output_dir : Path
|
|
702
|
-
Output directory path
|
|
703
|
-
"""
|
|
704
|
-
try:
|
|
705
|
-
logger.info("Exporting best PSF parameters as simulation model parameter files")
|
|
706
|
-
psf_pars_with_units = _add_units_to_psf_parameters(best_pars)
|
|
707
|
-
parameter_output_path = output_dir / tel_model.name
|
|
708
|
-
for parameter_name, parameter_value in psf_pars_with_units.items():
|
|
709
|
-
writer.ModelDataWriter.dump_model_parameter(
|
|
710
|
-
parameter_name=parameter_name,
|
|
711
|
-
value=parameter_value,
|
|
712
|
-
instrument=tel_model.name,
|
|
713
|
-
parameter_version=parameter_version,
|
|
714
|
-
output_file=f"{parameter_name}-{parameter_version}.json",
|
|
715
|
-
output_path=parameter_output_path,
|
|
716
|
-
use_plain_output_path=True,
|
|
717
|
-
)
|
|
718
|
-
logger.info(f"simulation model parameter files exported to {output_dir}")
|
|
719
|
-
except ImportError as e:
|
|
720
|
-
logger.warning(f"Could not export simulation parameters: {e}")
|
|
721
|
-
except (ValueError, KeyError, OSError) as e:
|
|
722
|
-
logger.error(f"Error exporting simulation parameters: {e}")
|
|
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
|
+
)
|
|
723
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
|
+
)
|
|
724
1125
|
|
|
725
|
-
def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
726
|
-
"""
|
|
727
|
-
Run the complete PSF parameter optimization workflow.
|
|
728
1126
|
|
|
729
|
-
|
|
1127
|
+
def write_monte_carlo_analysis(
|
|
1128
|
+
mc_results, output_dir, tel_model, use_ks_statistic=False, fraction=DEFAULT_FRACTION
|
|
1129
|
+
):
|
|
1130
|
+
"""
|
|
1131
|
+
Write Monte Carlo uncertainty analysis results to a log file.
|
|
730
1132
|
|
|
731
1133
|
Parameters
|
|
732
1134
|
----------
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
site_model : SiteModel
|
|
736
|
-
Site model object
|
|
737
|
-
args_dict : dict
|
|
738
|
-
Dictionary containing parsed command-line arguments
|
|
1135
|
+
mc_results : tuple
|
|
1136
|
+
Tuple of Monte Carlo analysis results from analyze_monte_carlo_error().
|
|
739
1137
|
output_dir : Path
|
|
740
|
-
|
|
1138
|
+
Directory where the log file will be written.
|
|
1139
|
+
tel_model : TelescopeModel
|
|
1140
|
+
Telescope model object for naming the output file.
|
|
1141
|
+
use_ks_statistic : bool, optional
|
|
1142
|
+
If True, analyze KS statistic results; if False, analyze RMSD results (default: False).
|
|
1143
|
+
fraction : float, optional
|
|
1144
|
+
PSF containment fraction for labeling (default: 0.8).
|
|
741
1145
|
|
|
742
1146
|
Returns
|
|
743
1147
|
-------
|
|
744
|
-
|
|
745
|
-
|
|
1148
|
+
Path
|
|
1149
|
+
Path to the created log file.
|
|
746
1150
|
"""
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1151
|
+
(
|
|
1152
|
+
mean_metric,
|
|
1153
|
+
std_metric,
|
|
1154
|
+
metric_values,
|
|
1155
|
+
mean_p_value,
|
|
1156
|
+
std_p_value,
|
|
1157
|
+
p_values,
|
|
1158
|
+
mean_psf_diameter,
|
|
1159
|
+
std_psf_diameter,
|
|
1160
|
+
psf_diameter_values,
|
|
1161
|
+
) = mc_results
|
|
1162
|
+
|
|
1163
|
+
metric_name = "KS Statistic" if use_ks_statistic else "RMSD"
|
|
1164
|
+
file_suffix = "ks" if use_ks_statistic else "rmsd"
|
|
1165
|
+
mc_file = output_dir.joinpath(f"monte_carlo_{file_suffix}_analysis_{tel_model.name}.log")
|
|
1166
|
+
|
|
1167
|
+
psf_label = get_psf_diameter_label(fraction)
|
|
1168
|
+
|
|
1169
|
+
with open(mc_file, "w", encoding="utf-8") as f:
|
|
1170
|
+
header = _create_log_header_and_format_value(
|
|
1171
|
+
f"Monte Carlo {metric_name} Error Analysis",
|
|
1172
|
+
tel_model,
|
|
1173
|
+
{"Number of simulations": len(metric_values)},
|
|
1174
|
+
)
|
|
1175
|
+
f.write(header)
|
|
750
1176
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1177
|
+
f.write(
|
|
1178
|
+
f"MONTE CARLO SIMULATION RESULTS:\nNumber of successful simulations: "
|
|
1179
|
+
f"{len(metric_values)}\n\n"
|
|
1180
|
+
)
|
|
1181
|
+
f.write(f"{metric_name.upper()} STATISTICS:\n")
|
|
1182
|
+
f.write(
|
|
1183
|
+
f"Mean {metric_name.lower()}: {mean_metric:.6f}\n"
|
|
1184
|
+
f"Standard deviation: {std_metric:.6f}\n"
|
|
1185
|
+
f"Minimum {metric_name.lower()}: {min(metric_values):.6f}\n"
|
|
1186
|
+
f"Maximum {metric_name.lower()}: {max(metric_values):.6f}\n"
|
|
1187
|
+
f"Relative error: {(std_metric / mean_metric) * 100:.2f}%\n\n"
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
if use_ks_statistic and mean_p_value is not None:
|
|
1191
|
+
valid_p_values = [p for p in p_values if p is not None]
|
|
1192
|
+
f.write(
|
|
1193
|
+
f"P-VALUE STATISTICS:\nMean p-value: {mean_p_value:.6f}\n"
|
|
1194
|
+
f"Standard deviation: {std_p_value:.6f}\n"
|
|
1195
|
+
f"Minimum p-value: {min(valid_p_values):.6f}\n"
|
|
1196
|
+
f"Maximum p-value: {max(valid_p_values):.6f}\n"
|
|
1197
|
+
f"Relative error: {(std_p_value / mean_p_value) * 100:.2f}%\n"
|
|
1198
|
+
)
|
|
755
1199
|
|
|
1200
|
+
good_fits = sum(1 for p in valid_p_values if p > 0.05)
|
|
1201
|
+
fair_fits = sum(1 for p in valid_p_values if 0.01 < p <= 0.05)
|
|
1202
|
+
poor_fits = sum(1 for p in valid_p_values if p <= 0.01)
|
|
1203
|
+
f.write(
|
|
1204
|
+
f"Good fits (p > 0.05): {good_fits}/{len(valid_p_values)} "
|
|
1205
|
+
f"({100 * good_fits / len(valid_p_values):.1f}%)\n"
|
|
1206
|
+
f"Fair fits (0.01 < p <= 0.05): {fair_fits}/{len(valid_p_values)} "
|
|
1207
|
+
f"({100 * fair_fits / len(valid_p_values):.1f}%)\n"
|
|
1208
|
+
f"Poor fits (p <= 0.01): {poor_fits}/{len(valid_p_values)} "
|
|
1209
|
+
f"({100 * poor_fits / len(valid_p_values):.1f}%)\n\n"
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
f.write(
|
|
1213
|
+
f"{psf_label} STATISTICS:\nMean {psf_label}: {mean_psf_diameter:.6f} cm\n"
|
|
1214
|
+
f"Standard deviation: {std_psf_diameter:.6f} cm\n"
|
|
1215
|
+
f"Minimum {psf_label}: {min(psf_diameter_values):.6f} cm\n"
|
|
1216
|
+
f"Maximum {psf_label}: {max(psf_diameter_values):.6f} cm\n"
|
|
1217
|
+
f"Relative error: {(std_psf_diameter / mean_psf_diameter) * 100:.2f}%\n\n"
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
f.write("INDIVIDUAL SIMULATION RESULTS:\n")
|
|
1221
|
+
for i, (metric_val, p_value, psf_diameter) in enumerate(
|
|
1222
|
+
zip(metric_values, p_values, psf_diameter_values)
|
|
1223
|
+
):
|
|
1224
|
+
if use_ks_statistic and p_value is not None:
|
|
1225
|
+
if p_value > 0.05:
|
|
1226
|
+
significance = "GOOD"
|
|
1227
|
+
elif p_value > 0.01:
|
|
1228
|
+
significance = "FAIR"
|
|
1229
|
+
else:
|
|
1230
|
+
significance = "POOR"
|
|
1231
|
+
f.write(
|
|
1232
|
+
f"Simulation {i + 1:2d}: {metric_name}={metric_val:.6f}, "
|
|
1233
|
+
f"p_value={p_value:.6f} ({significance}), {psf_label}={psf_diameter:.6f} cm\n"
|
|
1234
|
+
)
|
|
1235
|
+
else:
|
|
1236
|
+
f.write(
|
|
1237
|
+
f"Simulation {i + 1:2d}: {metric_name}={metric_val:.6f}, "
|
|
1238
|
+
f"{psf_label}={psf_diameter:.6f} cm\n"
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
return mc_file
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def _handle_monte_carlo_analysis(
|
|
1245
|
+
tel_model, site_model, args_dict, data_to_plot, radius, output_dir, use_ks_statistic
|
|
1246
|
+
):
|
|
1247
|
+
"""Handle Monte Carlo analysis if requested."""
|
|
1248
|
+
if not args_dict.get("monte_carlo_analysis", False):
|
|
1249
|
+
return False
|
|
1250
|
+
|
|
1251
|
+
mc_results = analyze_monte_carlo_error(tel_model, site_model, args_dict, data_to_plot, radius)
|
|
1252
|
+
if mc_results[0] is not None:
|
|
1253
|
+
mc_file = write_monte_carlo_analysis(
|
|
1254
|
+
mc_results,
|
|
1255
|
+
output_dir,
|
|
1256
|
+
tel_model,
|
|
1257
|
+
use_ks_statistic,
|
|
1258
|
+
args_dict.get("fraction", DEFAULT_FRACTION),
|
|
1259
|
+
)
|
|
1260
|
+
logger.info(f"Monte Carlo analysis results written to {mc_file}")
|
|
1261
|
+
mc_plot_file = output_dir.joinpath(f"monte_carlo_uncertainty_{tel_model.name}.pdf")
|
|
1262
|
+
plot_psf.create_monte_carlo_uncertainty_plot(
|
|
1263
|
+
mc_results, mc_plot_file, args_dict.get("fraction", DEFAULT_FRACTION), use_ks_statistic
|
|
1264
|
+
)
|
|
1265
|
+
return True
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def run_psf_optimization_workflow(tel_model, site_model, args_dict, output_dir):
|
|
1269
|
+
"""Run the complete PSF parameter optimization workflow using gradient descent."""
|
|
756
1270
|
data_to_plot, radius = load_and_process_data(args_dict)
|
|
1271
|
+
use_ks_statistic = args_dict.get("ks_statistic", False)
|
|
757
1272
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1273
|
+
if _handle_monte_carlo_analysis(
|
|
1274
|
+
tel_model, site_model, args_dict, data_to_plot, radius, output_dir, use_ks_statistic
|
|
1275
|
+
):
|
|
1276
|
+
return
|
|
762
1277
|
|
|
763
|
-
#
|
|
764
|
-
|
|
765
|
-
|
|
1278
|
+
# Run gradient descent optimization
|
|
1279
|
+
threshold = args_dict.get("rmsd_threshold")
|
|
1280
|
+
learning_rate = args_dict.get("learning_rate")
|
|
1281
|
+
|
|
1282
|
+
best_pars, best_psf_diameter, gd_results = run_gradient_descent_optimization(
|
|
1283
|
+
tel_model,
|
|
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,
|
|
766
1291
|
)
|
|
767
1292
|
|
|
768
|
-
|
|
769
|
-
|
|
1293
|
+
# Check if optimization was successful
|
|
1294
|
+
if not gd_results or best_pars is None:
|
|
1295
|
+
logger.error("Gradient descent optimization failed. No valid results found.")
|
|
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
|
+
)
|
|
1301
|
+
return
|
|
1302
|
+
|
|
1303
|
+
plot_psf.create_optimization_plots(args_dict, gd_results, tel_model, data_to_plot, output_dir)
|
|
770
1304
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1305
|
+
convergence_plot_file = output_dir.joinpath(
|
|
1306
|
+
f"gradient_descent_convergence_{tel_model.name}.png"
|
|
1307
|
+
)
|
|
1308
|
+
plot_psf.create_gradient_descent_convergence_plot(
|
|
1309
|
+
gd_results,
|
|
1310
|
+
threshold,
|
|
1311
|
+
convergence_plot_file,
|
|
1312
|
+
args_dict.get("fraction", DEFAULT_FRACTION),
|
|
1313
|
+
use_ks_statistic,
|
|
774
1314
|
)
|
|
775
|
-
print(f"\nParameter results written to {param_file}")
|
|
776
1315
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1316
|
+
param_file = write_gradient_descent_log(
|
|
1317
|
+
gd_results,
|
|
1318
|
+
best_pars,
|
|
1319
|
+
best_psf_diameter,
|
|
1320
|
+
output_dir,
|
|
1321
|
+
tel_model,
|
|
1322
|
+
use_ks_statistic,
|
|
1323
|
+
args_dict.get("fraction", DEFAULT_FRACTION),
|
|
1324
|
+
)
|
|
1325
|
+
logger.info(f"\nGradient descent progression written to {param_file}")
|
|
780
1326
|
|
|
781
|
-
|
|
782
|
-
for par, value in best_pars.items():
|
|
783
|
-
print(f"{par} = {value}")
|
|
1327
|
+
plot_psf.create_psf_vs_offaxis_plot(tel_model, site_model, args_dict, best_pars, output_dir)
|
|
784
1328
|
|
|
785
|
-
# Export best parameters as simulation model parameter files (if flag is provided)
|
|
786
1329
|
if args_dict.get("write_psf_parameters", False):
|
|
1330
|
+
logger.info("Exporting best parameters as model files...")
|
|
787
1331
|
export_psf_parameters(
|
|
788
|
-
best_pars,
|
|
789
|
-
tel_model,
|
|
790
|
-
args_dict.get("parameter_version", "0.0.0"),
|
|
791
|
-
output_dir.parent,
|
|
1332
|
+
best_pars, args_dict.get("telescope"), args_dict.get("parameter_version"), output_dir
|
|
792
1333
|
)
|