Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
b5a061c
gradient descent mrra
EshitaJoshi Nov 18, 2025
2dd86b5
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Nov 18, 2025
d894301
changelog
EshitaJoshi Nov 18, 2025
b759b11
updating tests
EshitaJoshi Nov 18, 2025
abd0ce8
remove duplications in mocks
EshitaJoshi Nov 18, 2025
fb88fb6
copilot review
EshitaJoshi Nov 18, 2025
3bc22ba
updating integration tests
EshitaJoshi Nov 18, 2025
a929462
fix tests
EshitaJoshi Nov 19, 2025
a0b12a7
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Nov 19, 2025
1182e7e
remove duplication
EshitaJoshi Nov 19, 2025
252e240
fixing unit test
EshitaJoshi Nov 19, 2025
b8d2d23
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Jan 5, 2026
0fcb0a7
per mirror d80
EshitaJoshi Jan 8, 2026
8bfc0fc
single mirror optimisation
EshitaJoshi Jan 13, 2026
c7617da
parallelisation
EshitaJoshi Jan 13, 2026
c394aa6
fixing overwrites
EshitaJoshi Jan 14, 2026
abbd5fa
d80 hist plots
EshitaJoshi Jan 14, 2026
edda1bc
tests
EshitaJoshi Jan 15, 2026
1f01128
central finite difference
EshitaJoshi Jan 15, 2026
f56d54a
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Jan 15, 2026
bf9f6d6
fixing import
EshitaJoshi Jan 15, 2026
0c701ee
fixing tests
EshitaJoshi Jan 15, 2026
794e2ea
fix sonarqube
EshitaJoshi Jan 15, 2026
c050df5
sonarqube
EshitaJoshi Jan 15, 2026
a6a9a65
round final values
EshitaJoshi Jan 15, 2026
d9b668a
cleaning up
EshitaJoshi Jan 16, 2026
0118610
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Jan 16, 2026
c43cc32
copilot review
EshitaJoshi Jan 16, 2026
99ceb55
moving plotting
EshitaJoshi Jan 27, 2026
ef72706
review feedback
EshitaJoshi Jan 27, 2026
e4db058
review feedback
EshitaJoshi Jan 27, 2026
4082bde
moving workers out
EshitaJoshi Jan 27, 2026
7d0729b
fixing docstrings
EshitaJoshi Jan 27, 2026
eca5e2c
updating tests
EshitaJoshi Jan 27, 2026
72d691e
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Jan 27, 2026
5b5be52
cleaning up code
EshitaJoshi Jan 28, 2026
c48e5b9
merge conflict
EshitaJoshi Jan 28, 2026
44a244c
linting error
EshitaJoshi Jan 28, 2026
6bdb7b4
updating tests
EshitaJoshi Jan 28, 2026
7266d42
fixing tests
EshitaJoshi Jan 28, 2026
cdd7e8a
adding label to ray tracing
EshitaJoshi Jan 28, 2026
56ac756
changelog
EshitaJoshi Jan 28, 2026
aceb3d1
typo
EshitaJoshi Jan 28, 2026
ec81fa7
Delete tests/resources/198mir_190925.ecsv
EshitaJoshi Jan 29, 2026
1ddd5ec
copilot review
EshitaJoshi Jan 29, 2026
58b6356
Merge branch 'mrra-gradient-descent' into ray-tracing-label
EshitaJoshi Jan 29, 2026
3625ab6
Merge pull request #1999 from gammasim/ray-tracing-label
EshitaJoshi Jan 29, 2026
8990cda
review feedback
EshitaJoshi Feb 2, 2026
512f135
Merge branch 'main' into mrra-gradient-descent
EshitaJoshi Feb 2, 2026
3627110
renaming d80 to psf
EshitaJoshi Feb 2, 2026
0434ae0
Merge branch 'mrra-gradient-descent' of https://github.com/gammasim/s…
EshitaJoshi Feb 2, 2026
10ed2f9
renaming d80 to psf
EshitaJoshi Feb 2, 2026
3495eb0
code cov
EshitaJoshi Feb 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes/1911.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement gradient descent for the derivation of mirror_reflection_random_angle. Implement general tools for multiprocessing.
1 change: 1 addition & 0 deletions docs/changes/1999.maintenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the ability to pass a label to RayTracing instead of using a cached label from the telescope model.
7 changes: 7 additions & 0 deletions docs/source/api-reference/job_execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ This is mostly used for small productions during the validation or verification
.. automodule:: job_execution.htcondor_script_generator
:members:
```

(process-pool)=

```{eval-rst}
.. automodule:: job_execution.process_pool
:members:
```
288 changes: 111 additions & 177 deletions src/simtools/applications/derive_mirror_rnda.py
Original file line number Diff line number Diff line change
@@ -1,227 +1,161 @@
#!/usr/bin/python3

r"""
Derive mirror random reflection angle (mirror roughness) of a single mirror panel.

Description
-----------

This application derives the value of the simulation model parameter
*mirror_reflection_random_angle* using measurements of the focal length
and point-spread function (PSF) of individual mirror panels.
This parameter is sometimes referred to as the "mirror roughness".

PSF measurements are provided by one of the following options:

* mean and sigma value obtained from the measurement of containment diameters of a number of
mirror panels in cm (``--psf_measurement_containment_mean`` and
``--psf_measurement_containment_sigma``)
* file (table) with measured PSF for each mirror panel spot size (``--psf_measurement``)

The containment fraction used for the PSF diameter calculation is set through
the argument ``--containment_fraction`` (typically 0.8 = 80%; called below D80).

Mirror panels are simulated individually, using one of the following options to set the
mirror panel focal length:

* file (table) with measured focal lengths per mirror panel
(provided through ``--mirror_list``)
* randomly generated focal lengths using an expected spread (value given through
``--random_focal_length``) around the mean focal length (provided through the
Model Parameters DB). This option is switched with ``--use_random_focal_length``.

The tuning algorithm requires a starting value for the random reflection angle. This is either
taken from the Model Parameters DB (default) or can be set using the argument ``--rnda``.

Ray-tracing simulations are performed for single mirror configurations for each
mirror given in the mirror list. The mean simulated containment diameter for all the mirrors
is compared with the mean measured containment diameter. The algorithm defines a new value for
the random reflection angle based on the sign of the difference between measured and simulated
containment diameters and a new set of simulations is performed. This process is repeated
until the sign of the difference changes, meaning that the two final values of the random
reflection angle brackets the optimal. These two values are used to find the optimal one by
a linear interpolation. Finally, simulations are performed by using the interpolated value,
which is defined as the desired optimal.

The option ``--no_tuning`` can be used if one only wants to simulate one value for the random
reflection angle and compare the results with the measured ones.

Results of the tuning are plotted. See examples of the PSF containment diameter
D80 vs random reflection angle plot, on the left, and the D80 distributions
(per mirror panel), on the right.

.. _derive_rnda_plot:
.. image:: images/derive_mirror_rnda_North-MST-FlashCam-D.png
:width: 49 %
.. image:: images/derive_mirror_rnda_North-MST-FlashCam-D_D80-distributions.png
:width: 49 %

This application uses the following software tools:

- sim_telarray/bin/sim_telarray
- sim_telarray/bin/rx (optional)

Command line arguments
----------------------
telescope (str, required)
Telescope name (e.g. LSTN-01, SSTS-25)
model_version (str, optional)
Model version
psf_measurement (str, optional)
Table with results from PSF measurements for each mirror panel spot size
psf_measurement_containment_mean (float, required)
Mean of measured containment diameter [cm]
psf_measurement_containment_sigma (float, optional)
Std dev of measured containment diameter [cm]
containment_fraction (float, required)
Containment fraction for diameter calculation
rnda (float, optional)
Starting value of mirror_reflection_random_angle [deg]. If not given, the value from the
default model is read from the simulation model database.
mirror_list (file, optional)
Table with mirror ID and panel radius.
use_random_focal_length (activation mode, optional)
Use random focal lengths, instead of the measured ones. The argument random_focal_length
can be used to replace the default random_focal_length from the model.
random_focal_length (float, optional)
Value of the random focal lengths to replace the default random_focal_length. Only used if
'use_random_focal_length' is activated.
random_focal_length_seed (int, optional)
Seed for the random number generator used for focal length variation.
no_tuning (activation mode, optional)
Turn off the tuning - A single case will be simulated and plotted.
test (activation mode, optional)
If activated, application will be faster by simulating only few mirrors.

Example
-------
Derive mirror random reflection angle for a large-sized telescope (LSTS),
simulation production 6.0.0

.. code-block:: console

simtools-derive-mirror-rnda \\
--site South \\
--telescope LSTS-design \\
--model_version 6.0.0 \\
--containment_fraction 0.8 \\
--mirror_list ./tests/resources/mirror_list_CTA-N-LST1_v2019-03-31_rotated.ecsv
--rnda 0.003 \\
--psf_measurement_containment_mean 1.4 \\

Expected final print-out message:

.. code-block:: console

Measured D80:
Mean = 1.400 cm

Simulated D80:
Mean = 1.406 cm, StdDev = 0.005 cm

mirror_random_reflection_angle
Previous value = 0.003000
New value = 0.003824
r"""Derive mirror random reflection angle based on per-mirror PSF diameter optimization.

Description
-----------

This application derives the value of the simulation model parameter
*mirror_reflection_random_angle* using measurements of a PSF containment diameter
and focal length of individual mirror panels.

The optimization uses percentage difference as the metric::

pct_diff = 100 * (simulated_psf - measured_psf) / measured_psf

Each mirror is optimized individually, and the final RNDA is the average of all
Comment thread
EshitaJoshi marked this conversation as resolved.
per-mirror optimized values.

Command line arguments
----------------------

site (str, required)
North or South.
telescope (str, required)
Telescope name (e.g. LSTN-01, SSTS-25).
model_version (str, optional)
Model version.
data (str, required)
ECSV file with PSF diameter (mm) per mirror.
Accepted column names: psf_opt, psf, or d80.
fraction (float, optional)
PSF containment fraction for diameter calculation (e.g. 0.8 for D80, 0.95 for D95).
Default: 0.8.
threshold (float, optional)
Convergence threshold for percentage difference (e.g. 0.05 for 5%).
Default: 0.05.
learning_rate (float, optional)
Learning rate for gradient descent. Default: 0.001.
test (optional)
Only optimize a small number of mirrors.
n_workers (int, optional)
Number of parallel worker processes to use. Default: 0 (auto chooses maximum).
number_of_mirrors_to_test (int, optional)
Number of mirrors to optimize when --test is used. Default: 10.
psf_hist (str, optional)
If activated, write a histogram comparing measured vs simulated PSF diameter distributions.
cleanup (optional)
Remove intermediate files (patterns: ``*.log``, ``*.lis*``, ``*.dat``)
from output.

Example
-------

.. code-block:: console

simtools-derive-mirror-rnda \
--site North \
--telescope LSTN-01 \
--model_version 7.0.0 \
--data tests/resources/MLTdata-preproduction.ecsv \
--parameter_version 1.0.0 \
--test --psf_hist --cleanup

"""

from pathlib import Path

from simtools.application_control import get_application_label, startup_application
from simtools.configuration import configurator
from simtools.ray_tracing.mirror_panel_psf import MirrorPanelPSF
from simtools.ray_tracing.psf_parameter_optimisation import cleanup_intermediate_files


def _parse():
"""Parse command line configuration."""
config = configurator.Configurator(
description="Derive mirror random reflection angle.", label=get_application_label(__file__)
description="Derive mirror RNDA using per-mirror PSF diameter optimization.",
label=get_application_label(__file__),
)
psf_group = config.parser.add_mutually_exclusive_group()
psf_group.add_argument(
"--psf_measurement_containment_mean",
help="Mean of measured PSF containment diameter [cm]",
type=float,
required=False,
)
psf_group.add_argument(
"--psf_measurement",
help="Results from PSF measurements for each mirror panel spot size",
config.parser.add_argument(
"--data",
help="ECSV file with a PSF diameter column (mm) per mirror",
type=str,
required=False,
required=True,
)
config.parser.add_argument(
"--psf_measurement_containment_sigma",
help="Std dev of measured PSF containment diameter [cm]",
"--threshold",
help="Convergence threshold for percentage difference.",
type=float,
required=False,
default=0.05,
)
config.parser.add_argument(
"--containment_fraction",
help="Containment fraction for diameter calculation (in interval 0,1)",
type=config.parser.efficiency_interval,
required=False,
default=0.8,
)
config.parser.add_argument(
"--rnda",
help="Starting value of mirror_reflection_random_angle",
"--learning_rate",
help="Learning rate for gradient descent.",
type=float,
required=False,
default=0.0,
)
config.parser.add_argument(
"--mirror_list",
help=("Mirror list file to replace the default one."),
type=str,
required=False,
default=0.001,
)
config.parser.add_argument(
"--rtol_psf_containment",
help="Relative tolerance for the containment diameter (default is 0.1).",
"--fraction",
help=(
"PSF containment fraction for diameter calculation (e.g., 0.8 for D80, 0.95 for D95)."
),
type=float,
required=False,
default=0.1,
default=0.8,
)
config.parser.add_argument(
"--use_random_focal_length",
help=("Use random focal lengths."),
action="store_true",
"--n_workers",
help="Number of parallel worker processes to use.",
type=int,
required=False,
default=0,
)
config.parser.add_argument(
"--random_focal_length",
help=(
"Value of the random focal length. Only used if 'use_random_focal_length' is activated."
),
default=None,
type=float,
"--number_of_mirrors_to_test",
help="Number of mirrors to optimize when --test is used.",
type=int,
required=False,
default=10,
)
config.parser.add_argument(
"--random_focal_length_seed",
help="Seed for the random number generator used for focal length variation.",
type=int,
required=False,
"--psf_hist",
nargs="?",
const="psf_distributions.png",
default=None,
help=(
"Write a histogram comparing measured vs simulated PSF diameter distributions. "
"Optionally provide a filename (relative to output dir unless absolute)."
),
)
config.parser.add_argument(
"--no_tuning",
help="no tuning of random_reflection_angle (a single case will be simulated).",
"--cleanup",
action="store_true",
required=False,
default=False,
help=(
"Remove intermediate files from the output directory (patterns: *.log, *.lis*, *.dat)."
),
)
return config.initialize(
db_config=True, output=True, simulation_model=["telescope", "model_version"]
db_config=True,
output=True,
simulation_model=["telescope", "model_version", "site", "parameter_version"],
)


def main():
"""Derive mirror random reflection angle of a single mirror panel."""
"""Derive mirror random reflection angle using per-mirror PSF diameter optimization."""
app_context = startup_application(_parse)

panel_psf = MirrorPanelPSF(app_context.args.get("label"), app_context.args)
panel_psf.derive_random_reflection_angle(save_figures=True)
panel_psf.print_results()
panel_psf.optimize_with_gradient_descent()
panel_psf.write_optimization_data()
if app_context.args.get("psf_hist"):
panel_psf.write_psf_histogram()

if app_context.args.get("cleanup"):
output_dir = Path(app_context.args.get("output_path", "."))
cleanup_intermediate_files(output_dir)


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions src/simtools/applications/validate_cumulative_psf.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def main():
ray = RayTracing(
telescope_model=tel_model,
site_model=site_model,
label=app_context.args.get("label"),
zenith_angle=app_context.args["zenith"] * u.deg,
source_distance=app_context.args["src_distance"] * u.km,
off_axis_angle=[0.0] * u.deg,
Expand Down
3 changes: 2 additions & 1 deletion src/simtools/applications/validate_optics.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def main():
ray = RayTracing(
telescope_model=tel_model,
site_model=site_model,
label=app_context.args.get("label") or Path(__file__).stem,
zenith_angle=app_context.args["zenith"] * u.deg,
source_distance=app_context.args["src_distance"] * u.km,
off_axis_angle=np.linspace(
Expand All @@ -147,7 +148,7 @@ def main():
ray.analyze(force=True)

# Plotting
for key in ["d80_deg", "d80_cm", "eff_area", "eff_flen"]:
for key in ["psf_deg", "psf_cm", "eff_area", "eff_flen"]:
plt.figure(figsize=(8, 6), tight_layout=True)

ray.plot(key, marker="o", linestyle=":", color="k")
Expand Down
Loading