#!/usr/bin/python3
r"""
Derives the mirror alignment parameters using cumulative PSF measurement.
This includes parameters mirror_reflection_random_angle, \
mirror_align_random_horizontal and mirror_align_random_vertical.
The measured cumulative PSF should be provided by using the command line argument data. \
A file name is expected, in which the file should contain 3 columns: radial distance in mm, \
differential value of photon intensity and its integral value.
The derivation is performed through gradient descent optimization that minimizes either the \
Root Mean Squared Deviation (RMSD) between measured and simulated PSF curves (default) or the \
Kolmogorov-Smirnov (KS) statistic when the --ks_statistic flag is used.
The optimization workflow includes:
* Loading and preprocessing PSF data from measurement files
* Running gradient descent optimization to minimize RMSD
* Generating cumulative PSF plots for each iteration showing optimization progression
* Logging parameter evolution through gradient descent steps
* Creating convergence plots showing RMSD and D80 evolution
* Automatically generating D80 vs off-axis angle analysis for best parameters
* Optionally exporting optimized parameters as simulation model files
The assumption are:
a) mirror_align_random_horizontal and mirror_align_random_vertical are the same.
b) mirror_align_random_horizontal/vertical have no dependence on the zenith angle.
One example of the plot generated by this applications are shown below.
.. _derive_psf_parameters_plot:
.. image:: images/gradient_descent.png
:width: 49 %
Command line arguments
----------------------
site (str, required)
North or South.
telescope (str, required)
Telescope model name (e.g. LST-1, SST-D, ...).
model_version (str, optional)
Model version.
parameter_version (str, optional)
Parameter version for model parameter file export.
src_distance (float, optional)
Source distance in km.
zenith (float, optional)
Zenith angle in deg.
data (str, optional)
Name of the data file with the measured cumulative PSF.
plot_all (activation mode, optional)
If activated, plots will be generated for all values tested during tuning.
fixed (activation mode, optional)
Keep the first entry of mirror_reflection_random_angle fixed.
test (activation mode, optional)
If activated, application will be faster by simulating fewer photons.
write_psf_parameters (activation mode, optional)
Write the optimized PSF parameters as simulation model parameter files.
rmsd_threshold (float, optional)
RMSD threshold for gradient descent convergence (default: 0.007).
learning_rate (float, optional)
Learning rate for gradient descent optimization (default: 0.01).
monte_carlo_analysis (activation mode, optional)
Run Monte Carlo analysis to find statistical uncertainties.
Example
-------
--telescope LSTN-01 --model_version 6.0.0
Run the application:
.. code-block:: console
simtools-derive-psf-parameters --site North --telescope LSTN-01 \\
--model_version 6.0.0 --data tests/resources/PSFcurve_data_v2.ecsv --plot_all --test
Run with parameter export:
.. code-block:: console
simtools-derive-psf-parameters --site North --telescope LSTN-01 --model_version 6.0.0 \\
--plot_all --test --rmsd_threshold 0.01 --learning_rate 0.001 \\
--data tests/resources/PSFcurve_data_v2.ecsv \\
--write_psf_parameters
Run monte carlo analysis:
.. code-block:: console
simtools-derive-psf-parameters --site North --telescope LSTN-01 --model_version 6.0.0 \\
--plot_all --test --monte_carlo_analysis \\
--data tests/resources/PSFcurve_data_v2.ecsv \\
--write_psf_parameters
The output is saved in simtools-output/derive_psf_parameters.
Output files include:
* Gradient descent progression log in psf_gradient_descent_[telescope].log
* Gradient descent convergence plots in gradient_descent_convergence_[telescope].png
* PSF progression plots showing evolution through iterations (if --plot_all is specified)
* D80 vs off-axis angle plots (d80_vs_offaxis_cm.png, d80_vs_offaxis_deg.png)
* Optimized simulation model parameter files (if --write_psf_parameters is specified)
"""
from simtools.application_control import get_application_label, startup_application
from simtools.configuration import configurator
from simtools.model.model_utils import initialize_simulation_models
from simtools.ray_tracing import psf_parameter_optimisation as psf_opt
def _parse():
config = configurator.Configurator(
label=get_application_label(__file__),
description=(
"Derive mirror_reflection_random_angle, mirror_align_random_horizontal "
"and mirror_align_random_vertical using cumulative PSF measurement."
),
)
config.parser.add_argument(
"--src_distance",
help="Source distance in km",
type=float,
default=10,
)
config.parser.add_argument("--zenith", help="Zenith angle in deg", type=float, default=20)
config.parser.add_argument(
"--data", help="Data file name with the measured PSF vs radius [cm]", type=str
)
config.parser.add_argument(
"--plot_all",
help=(
"On: plot cumulative PSF for all tested combinations, "
"Off: plot it only for the best set of values"
),
action="store_true",
)
config.parser.add_argument(
"--fixed",
help=("Keep the first entry of mirror_reflection_random_angle fixed."),
action="store_true",
)
config.parser.add_argument(
"--write_psf_parameters",
help=("Write the optimized PSF parameters as simulation model parameter files"),
action="store_true",
required=False,
)
config.parser.add_argument(
"--rmsd_threshold",
help=(
"RMSD threshold for gradient descent convergence "
"(not used with --monte_carlo_analysis)."
),
type=float,
default=0.01,
)
config.parser.add_argument(
"--learning_rate",
help=(
"Learning rate for gradient descent optimization "
"(not used with --monte_carlo_analysis)."
),
type=float,
default=0.01,
)
config.parser.add_argument(
"--monte_carlo_analysis",
help="Run analysis to find monte carlo uncertainties.",
action="store_true",
)
config.parser.add_argument(
"--ks_statistic",
help="Use KS statistic for monte carlo uncertainty analysis.",
action="store_true",
)
config.parser.add_argument(
"--fraction",
help="PSF containment fraction for diameter calculation (e.g., 0.8 for D80, 0.95 for D95).",
type=float,
default=0.8,
)
return config.initialize(
db_config=True,
simulation_model=["telescope", "model_version", "parameter_version"],
)
[docs]
def main():
"""Derive PSF parameters."""
app_context = startup_application(_parse)
tel_model, site_model, _ = initialize_simulation_models(
label=app_context.args.get("label"),
db_config=app_context.db_config,
site=app_context.args["site"],
telescope_name=app_context.args["telescope"],
model_version=app_context.args["model_version"],
)
psf_opt.run_psf_optimization_workflow(
tel_model,
site_model,
app_context.args,
app_context.io_handler.get_output_directory(),
)
if __name__ == "__main__":
main()