Repository: gammasim/simtools
Pull request #1436: Add --all_* flags to automate report generation
Author: login: EshitaJoshi
Created: 2025-03-14T14:39:49Z | Merged: 2025-03-24T13:30:38Z

This PR adds the following flags that can be used in the command line as arguments:

  ```
  --all_telescopes
  --all_sites
  --all_model_versions
  ```

The three flags can be used individually like

`python src/simtools/applications/docs_produce_array_element_report.py --all_telescopes --site South --model_version 6.0.0`

`python src/simtools/applications/docs_produce_model_parameter_reports.py --telescope MSTx-FlashCam --all_sites`

or all together like

`python src/simtools/applications/docs_produce_array_element_report.py --all_telescopes --all_sites --all_model_versions`

`python src/simtools/applications/docs_produce_model_parameter_reports.py --all_telescopes --all_sites`


Diff:
diff --git a/docs/changes/1436.feature.md b/docs/changes/1436.feature.md
new file mode 100644
index 0000000000..8fe3bae6b5
--- /dev/null
+++ b/docs/changes/1436.feature.md
@@ -0,0 +1 @@
+Added functionality to automate report generation for array elements and parameters.
diff --git a/docs/source/api-reference/reporting.md b/docs/source/api-reference/reporting.md
index cada6c6c1a..1f7f638d07 100644
--- a/docs/source/api-reference/reporting.md
+++ b/docs/source/api-reference/reporting.md
@@ -11,3 +11,11 @@ The reporting modules read model parameter values and descriptions from the data
 .. automodule:: reporting.docs_read_parameters
    :members:
 ```
+
+
+## docs_auto_report_generator
+
+```{eval-rst}
+.. automodule:: reporting.docs_auto_report_generator
+   :members:
+```
diff --git a/src/simtools/applications/docs_produce_array_element_report.py b/src/simtools/applications/docs_produce_array_element_report.py
index 6ddc16b17b..0ad4625687 100644
--- a/src/simtools/applications/docs_produce_array_element_report.py
+++ b/src/simtools/applications/docs_produce_array_element_report.py
@@ -9,9 +9,11 @@
 """
 
 import logging
+from pathlib import Path
 
 from simtools.configuration import configurator
 from simtools.io_operations import io_handler
+from simtools.reporting.docs_auto_report_generator import ReportGenerator
 from simtools.reporting.docs_read_parameters import ReadParameters
 from simtools.utils import general as gen
 
@@ -24,38 +26,55 @@ def _parse(label):
     )
 
     config.parser.add_argument(
-        "--parameter",
+        "--all_telescopes",
         action="store_true",
-        help="Compare all parameters across model versions for one telescope.",
+        help="Produce reports for all telescopes.",
+    )
+
+    config.parser.add_argument(
+        "--all_model_versions",
+        action="store_true",
+        help="Produce reports for all model versions.",
+    )
+
+    config.parser.add_argument(
+        "--all_sites", action="store_true", help="Produce reports for all sites."
     )
 
     return config.initialize(
-        db_config=True, simulation_model=["site", "telescope", "model_version", "parameter_version"]
+        db_config=True, simulation_model=["site", "telescope", "model_version"]
     )
 
 
 def main():  # noqa: D103
     label_name = "reports"
     args, db_config = _parse(label_name)
+
     io_handler_instance = io_handler.IOHandler()
-    output_path = io_handler_instance.get_output_directory(
-        label=label_name, sub_dir=f"productions/{args['model_version']}"
-    )
+    output_path = io_handler_instance.get_output_directory(label=label_name, sub_dir="productions")
 
     logger = logging.getLogger()
     logger.setLevel(gen.get_log_level_from_user(args["log_level"]))
 
-    ReadParameters(
-        db_config,
-        args,
-        output_path,
-    ).produce_array_element_report()
+    if any([args.get("all_telescopes"), args.get("all_sites"), args.get("all_model_versions")]):
+        ReportGenerator(
+            db_config,
+            args,
+            output_path,
+        ).auto_generate_array_element_reports()
 
-    logger.info(
-        f"Markdown report generated for {args['site']}"
-        f" Telescope {args['telescope']} (v{args['model_version']}):"
-        f" {output_path}"
-    )
+    else:
+        ReadParameters(
+            db_config,
+            args,
+            Path(output_path / f"{args['model_version']}"),
+        ).produce_array_element_report()
+
+        logger.info(
+            f"Markdown report generated for {args['site']}"
+            f" Telescope {args['telescope']} (v{args['model_version']}):"
+            f" {output_path}"
+        )
 
 
 if __name__ == "__main__":
diff --git a/src/simtools/applications/docs_produce_model_parameter_reports.py b/src/simtools/applications/docs_produce_model_parameter_reports.py
index 32cd304929..30057f14e0 100644
--- a/src/simtools/applications/docs_produce_model_parameter_reports.py
+++ b/src/simtools/applications/docs_produce_model_parameter_reports.py
@@ -12,6 +12,7 @@
 
 from simtools.configuration import configurator
 from simtools.io_operations import io_handler
+from simtools.reporting.docs_auto_report_generator import ReportGenerator
 from simtools.reporting.docs_read_parameters import ReadParameters
 from simtools.utils import general as gen
 
@@ -23,6 +24,16 @@ def _parse(label):
         description=("Produce a markdown report for model parameters."),
     )
 
+    config.parser.add_argument(
+        "--all_telescopes",
+        action="store_true",
+        help="Produce reports for all telescopes.",
+    )
+
+    config.parser.add_argument(
+        "--all_sites", action="store_true", help="Produce reports for all sites."
+    )
+
     return config.initialize(db_config=True, simulation_model=["site", "telescope"])
 
 
@@ -35,15 +46,24 @@ def main():  # noqa: D103
     logger = logging.getLogger()
     logger.setLevel(gen.get_log_level_from_user(args["log_level"]))
 
-    ReadParameters(
-        db_config,
-        args,
-        output_path,
-    ).produce_model_parameter_reports()
+    if any([args.get("all_telescopes"), args.get("all_sites")]):
+        ReportGenerator(
+            db_config,
+            args,
+            output_path,
+        ).auto_generate_parameter_reports()
 
-    logger.info(
-        f"Markdown report generated for {args['site']} Telescope {args['telescope']}: {output_path}"
-    )
+    else:
+        ReadParameters(
+            db_config,
+            args,
+            output_path,
+        ).produce_model_parameter_reports()
+
+        logger.info(
+            f"Markdown report generated for {args['site']}"
+            f"Telescope {args['telescope']}: {output_path}"
+        )
 
 
 if __name__ == "__main__":
diff --git a/src/simtools/db/db_handler.py b/src/simtools/db/db_handler.py
index f309c46658..e31e550b1e 100644
--- a/src/simtools/db/db_handler.py
+++ b/src/simtools/db/db_handler.py
@@ -273,8 +273,6 @@ def get_model_parameters_for_all_model_versions(self, site, array_element_name,
         """
         Get model parameters for all model versions.
 
-        Queries parameters for design and for the specified array element (if necessary).
-
         Parameters
         ----------
         site: str
@@ -290,10 +288,16 @@ def get_model_parameters_for_all_model_versions(self, site, array_element_name,
         """
         pars = defaultdict(dict)
         for _model_version in self.get_model_versions(collection):
-            parameter_data = self.get_model_parameters(
-                site, array_element_name, collection, _model_version
-            )
-            pars[_model_version].update(parameter_data)
+            try:
+                parameter_data = self.get_model_parameters(
+                    site, array_element_name, collection, _model_version
+                )
+                pars[_model_version].update(parameter_data)
+            except KeyError:
+                self._logger.debug(
+                    f"Skipping model version {_model_version} - {array_element_name} not found"
+                )
+                continue
         return pars
 
     def _get_parameter_for_model_version(
diff --git a/src/simtools/reporting/docs_auto_report_generator.py b/src/simtools/reporting/docs_auto_report_generator.py
new file mode 100644
index 0000000000..0a892fb403
--- /dev/null
+++ b/src/simtools/reporting/docs_auto_report_generator.py
@@ -0,0 +1,173 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _generate_array_element_report_combinations(self):
+        """Generate combinations of model versions, telescopes, and sites for array element reports.
+
+        This generator function yields tuples of (model_version, telescope, site) for array element
+        report generation based on the input arguments and flags.
+
+        The function filters telescopes by site and ensures that design models are included
+        when necessary.
+
+        Yields
+        ------
+            tuple[str, str, str]: A tuple containing (model_version, telescope, site) for each
+                valid combination based on the input arguments.
+        """
+        all_sites = names.site_names()
+
+        model_versions = (
+            self.db.get_model_versions()
+            if self.args.get("all_model_versions")
+            else [self.args["model_version"]]
+        )
+        selected_sites = all_sites if self.args.get("all_sites") else {self.args["site"]}
+
+        def get_telescopes(model_version):
+            """Get list of telescopes depending on input arguments."""
+            if not self.args.get("all_telescopes"):
+                return [self.args["telescope"]]
+            telescopes = self.db.get_array_elements(model_version)
+            all_telescopes = self._add_design_models_to_telescopes(model_version, telescopes)
+            return self._filter_telescopes_by_site(all_telescopes, selected_sites)
+
+        def get_valid_sites(telescope):
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            return [site for site in sites if site in selected_sites]
+
+        def generate_combinations():
+            for version in model_versions:
+                telescopes = get_telescopes(version)
+                for telescope in telescopes:
+                    for site in get_valid_sites(telescope):
+                        yield version, telescope, site
+
+        return generate_combinations()
+
+    def _generate_single_array_element_report(self, model_version, telescope, site):
+        """Generate a single report with given parameters."""
+        self.args.update(
+            {
+                "telescope": telescope,
+                "site": site,
+                "model_version": model_version,
+            }
+        )
+
+        output_path = Path(self.output_path) / str(model_version)
+        ReadParameters(self.db_config, self.args, output_path).produce_array_element_report()
+
+        logger.info(
+            f"Markdown report generated for {site} "
+            f"Telescope {telescope} (v{model_version}): {output_path}"
+        )
+
+    def auto_generate_array_element_reports(self):
+        """Generate all reports based on which --all_* flag is passed."""
+        for params in self._generate_array_element_report_combinations():
+            self._generate_single_array_element_report(*params)
+
+    def _get_telescopes_from_layout(self, site: str) -> set[str]:
+        """Get unique telescopes for a given site across all versions."""
+        if not self.args.get("all_telescopes"):
+            return {self.args["telescope"]}
+
+        # Get layouts for all versions for this site
+        layouts = self.db.get_model_parameters_for_all_model_versions(
+            site=site, array_element_name=f"OBS-{site}", collection="sites"
+        )
+
+        # Collect all telescopes from hyper array layouts
+        all_telescopes = set()
+        for key, version_data in layouts.items():
+            layout = version_data.get("array_layouts", {}).get("value", [])
+            hyper_array = next(
+                (layout["elements"] for layout in layout if layout["name"] == "hyper_array"), []
+            )
+            all_telescopes.update(hyper_array)
+            all_telescopes.update(self._add_design_models_to_telescopes(key, list(all_telescopes)))
+
+        return all_telescopes
+
+    def _get_valid_sites_for_telescope(self, telescope: str) -> list[str]:
+        """Get valid sites for a given telescope."""
+        sites = names.get_site_from_array_element_name(telescope)
+        return [sites] if not isinstance(sites, list) else sites
+
+    def _generate_parameter_report_combinations(self) -> Generator[tuple[str, str], None, None]:
+        """Generate combinations of telescopes and sites for report generation."""
+        if not self.args.get("all_telescopes"):
+            # For a specific telescope, get its valid sites regardless of --all_sites
+            telescope = self.args["telescope"]
+            for site in self._get_valid_sites_for_telescope(telescope):
+                yield telescope, site
+        else:
+            # For --all_telescopes, use selected sites to get telescopes
+            all_sites = names.site_names()
+            selected_sites = all_sites if self.args.get("all_sites") else {self.args["site"]}
+
+            for site in selected_sites:
+                telescopes = self._get_telescopes_from_layout(site)
+                yield from ((telescope, site) for telescope in telescopes)
+
+    def auto_generate_parameter_reports(self):
+        """Generate parameter reports for all telescope-site combinations."""
+        for telescope, site in self._generate_parameter_report_combinations():
+            self.args.update(
+                {
+                    "telescope": telescope,
+                    "site": site,
+                }
+            )
+
+            ReadParameters(
+                self.db_config, self.args, self.output_path
+            ).produce_model_parameter_reports()
+
+            logger.info(
+                f"Markdown report generated for {site} Telescope {telescope}: {self.output_path}"
+            )
diff --git a/src/simtools/reporting/docs_read_parameters.py b/src/simtools/reporting/docs_read_parameters.py
index 0ae4950159..2c43563ca6 100644
--- a/src/simtools/reporting/docs_read_parameters.py
+++ b/src/simtools/reporting/docs_read_parameters.py
@@ -11,7 +11,6 @@
 import numpy as np
 
 from simtools.db import db_handler
-from simtools.io_operations import io_handler
 from simtools.model.telescope_model import TelescopeModel
 from simtools.utils import names
 
@@ -70,84 +69,6 @@ def _convert_to_md(self, input_file):
 
         return f"_data_files/{output_file_name}"
 
-    def get_all_parameter_descriptions(self):
-        """
-        Get descriptions for all model parameters.
-
-        Returns
-        -------
-            tuple: A tuple containing two dictionaries:
-                - parameter_description: Maps parameter names to their descriptions.
-                - short_description: Maps parameter names to their short descriptions.
-                - inst_class: Maps parameter names to their respective class.
-        """
-        parameter_description, short_description, inst_class = {}, {}, {}
-
-        for instrument_class in names.db_collection_to_instrument_class_key("telescopes"):
-            for parameter, details in names.model_parameters(instrument_class).items():
-                parameter_description[parameter] = details.get("description")
-                short_description[parameter] = details.get("short_description")
-                inst_class[parameter] = instrument_class
-
-        return parameter_description, short_description, inst_class
-
-    def get_array_element_parameter_data(self, telescope_model, collection="telescopes"):
-        """
-        Get model parameter data and descriptions for a given array element.
-
-        Currently only configured for telescope.
-
-        Parameters
-        ----------
-        telescope_model : TelescopeModel
-            The telescope model instance.
-
-        Returns
-        -------
-        list: A list of lists containing parameter names, values with units,
-                descriptions, and short descriptions.
-        """
-        all_params = telescope_model.db.get_model_parameters(
-            site=telescope_model.site,
-            array_element_name=telescope_model.name,
-            collection=collection,
-            model_version=telescope_model.model_version,
-        )
-
-        telescope_model.export_model_files()
-        parameter_descriptions = self.get_all_parameter_descriptions()
-        data = []
-
-        for parameter in all_params:
-            if all_params[parameter]["instrument"] != telescope_model.name:
-                continue
-            parameter_version = telescope_model.get_parameter_version(parameter)
-            value = telescope_model.get_parameter_value_with_unit(parameter)
-            if telescope_model.get_parameter_file_flag(parameter) and value:
-                input_file_name = telescope_model.config_file_directory / Path(value)
-                output_file_name = self._convert_to_md(input_file_name)
-                value = f"[{Path(value).name}]({output_file_name})"
-            elif isinstance(value, list):
-                value = ", ".join(str(q) for q in value)
-            else:
-                value = str(value)
-
-            description = parameter_descriptions[0].get(parameter)
-            short_description = parameter_descriptions[1].get(parameter, description)
-            inst_class = parameter_descriptions[2].get(parameter)
-            data.append(
-                [
-                    inst_class,
-                    parameter,
-                    parameter_version,
-                    value,
-                    description,
-                    short_description,
-                ]
-            )
-
-        return data
-
     def _format_parameter_value(self, value_data, unit, file_flag):
         """Format parameter value based on type."""
         if file_flag:
@@ -216,14 +137,15 @@ def _compare_parameter_across_versions(self, all_param_data, all_parameter_names
 
         # Iterate over each model version
         for version in all_versions:
-            Path(f"{self.output_path}/model").mkdir(parents=True, exist_ok=True)
+            parameter_dict = all_param_data.get(version, {})
+            if not parameter_dict:
+                continue
 
+            Path(f"{self.output_path}/model").mkdir(parents=True, exist_ok=True)
             self.db.export_model_files(
                 parameters=all_param_data.get(version), dest=f"{self.output_path}/model"
             )
 
-            parameter_dict = all_param_data.get(version, {})
-
             for parameter_name in filter(parameter_dict.__contains__, all_parameter_names):
                 parameter_data = parameter_dict.get(parameter_name)
 
@@ -234,7 +156,7 @@ def _compare_parameter_across_versions(self, all_param_data, all_parameter_names
                 unit = parameter_data.get("unit") or " "
                 value_data = parameter_data.get("value")
 
-                if not value_data:
+                if value_data is None:
                     continue
 
                 file_flag = parameter_data.get("file", False)
@@ -254,6 +176,85 @@ def _compare_parameter_across_versions(self, all_param_data, all_parameter_names
 
         return self._group_model_versions_by_parameter_version(grouped_data)
 
+    def get_all_parameter_descriptions(self):
+        """
+        Get descriptions for all model parameters.
+
+        Returns
+        -------
+            tuple: A tuple containing two dictionaries:
+                - parameter_description: Maps parameter names to their descriptions.
+                - short_description: Maps parameter names to their short descriptions.
+                - inst_class: Maps parameter names to their respective class.
+        """
+        parameter_description, short_description, inst_class = {}, {}, {}
+
+        for instrument_class in names.db_collection_to_instrument_class_key("telescopes"):
+            for parameter, details in names.model_parameters(instrument_class).items():
+                parameter_description[parameter] = details.get("description")
+                short_description[parameter] = details.get("short_description")
+                inst_class[parameter] = instrument_class
+
+        return parameter_description, short_description, inst_class
+
+    def get_array_element_parameter_data(self, telescope_model, collection="telescopes"):
+        """
+        Get model parameter data and descriptions for a given array element.
+
+        Currently only configured for telescope.
+
+        Parameters
+        ----------
+        telescope_model : TelescopeModel
+            The telescope model instance.
+
+        Returns
+        -------
+        list: A list of lists containing parameter names, values with units,
+                descriptions, and short descriptions.
+        """
+        all_parameter_data = self.db.get_model_parameters(
+            site=telescope_model.site,
+            array_element_name=telescope_model.name,
+            collection=collection,
+            model_version=telescope_model.model_version,
+        )
+
+        Path(f"{self.output_path}/model").mkdir(parents=True, exist_ok=True)
+        self.db.export_model_files(parameters=all_parameter_data, dest=f"{self.output_path}/model")
+        parameter_descriptions = self.get_all_parameter_descriptions()
+        data = []
+
+        for parameter in filter(all_parameter_data.__contains__, names.model_parameters().keys()):
+            parameter_data = all_parameter_data.get(parameter)
+            if parameter_data["instrument"] != telescope_model.name:
+                continue
+            parameter_version = telescope_model.get_parameter_version(parameter)
+            unit = parameter_data.get("unit") or " "
+            value_data = parameter_data.get("value")
+
+            if value_data is None:
+                continue
+
+            file_flag = parameter_data.get("file", False)
+            value = self._format_parameter_value(value_data, unit, file_flag)
+
+            description = parameter_descriptions[0].get(parameter)
+            short_description = parameter_descriptions[1].get(parameter, description)
+            inst_class = parameter_descriptions[2].get(parameter)
+            data.append(
+                [
+                    inst_class,
+                    parameter,
+                    parameter_version,
+                    value,
+                    description,
+                    short_description,
+                ]
+            )
+
+        return data
+
     def produce_array_element_report(self):
         """
         Produce a markdown report of all model parameters per array element.
@@ -332,10 +333,8 @@ def produce_model_parameter_reports(self):
             f"Comparing parameters across model versions for Telescope: {self.array_element}"
             f" and Site: {self.site}."
         )
-        io_handler_instance = io_handler.IOHandler()
-        output_path = io_handler_instance.get_output_directory(
-            label="reports", sub_dir=f"parameters/{self.array_element}"
-        )
+        output_path = self.output_path / f"{self.array_element}"
+        Path(output_path).mkdir(parents=True, exist_ok=True)
 
         all_parameter_names = names.model_parameters(None).keys()
         all_parameter_data = self.db.get_model_parameters_for_all_model_versions(
diff --git a/tests/integration_tests/config/docs_produce_array_element_report_run.yml b/tests/integration_tests/config/docs_produce_array_element_report_run.yml
index f01544ae6e..b37d9ac451 100644
--- a/tests/integration_tests/config/docs_produce_array_element_report_run.yml
+++ b/tests/integration_tests/config/docs_produce_array_element_report_run.yml
@@ -7,9 +7,9 @@ CTA_SIMPIPE:
       SITE: North
       TELESCOPE: LSTN-01
       MODEL_VERSION: 6.0.0
-      OUTPUT_PATH: simtools-output/reports/productions/6.0.0
+      OUTPUT_PATH: simtools-output/reports/productions/
       USE_PLAIN_OUTPUT_PATH: true
     INTEGRATION_TESTS:
-    - OUTPUT_FILE: LSTN-01.md
+    - OUTPUT_FILE: 6.0.0/LSTN-01.md
 SCHEMA_NAME: application_workflow.metaschema
 SCHEMA_VERSION: 0.3.0
diff --git a/tests/integration_tests/config/docs_produce_model_parameter_reports_run.yml b/tests/integration_tests/config/docs_produce_model_parameter_reports_run.yml
index feef9ea46d..bd0a739bea 100644
--- a/tests/integration_tests/config/docs_produce_model_parameter_reports_run.yml
+++ b/tests/integration_tests/config/docs_produce_model_parameter_reports_run.yml
@@ -6,9 +6,9 @@ CTA_SIMPIPE:
     CONFIGURATION:
       SITE: North
       TELESCOPE: LSTN-01
-      OUTPUT_PATH: simtools-output/reports/parameters/LSTN-01
+      OUTPUT_PATH: simtools-output/reports/parameters/
       USE_PLAIN_OUTPUT_PATH: true
     INTEGRATION_TESTS:
-    - OUTPUT_FILE: quantum_efficiency.md
+    - OUTPUT_FILE: LSTN-01/quantum_efficiency.md
 SCHEMA_NAME: application_workflow.metaschema
 SCHEMA_VERSION: 0.3.0
diff --git a/tests/unit_tests/db/test_db_handler.py b/tests/unit_tests/db/test_db_handler.py
index 4805a205f7..46f09929f2 100644
--- a/tests/unit_tests/db/test_db_handler.py
+++ b/tests/unit_tests/db/test_db_handler.py
@@ -397,6 +397,61 @@ def test_get_model_parameters_for_all_model_versions(
     assert result == expected
 
 
+def test_get_model_parameters_for_all_model_versions_mst(
+    db,
+    mocker,
+    standard_test_params,
+):
+    """Test get_model_parameters_for_all_model_versions method."""
+    site = standard_test_params["site"]
+    array_element_name = "MSTN-101"  # Using telescope that only exists in prod6
+    collection = standard_test_params["collection"]
+
+    mock_get_model_versions = mocker.patch.object(
+        db, "get_model_versions", return_value=["5.0.0", "6.0.0"]
+    )
+
+    # Mock get_model_parameters to raise KeyError for version 5.0.0
+    # but return valid data for version 6.0.0
+    def mock_get_parameters(site, array_element_name, collection, version):
+        if version == "5.0.0":
+            raise KeyError(f"{array_element_name} not found")
+        return {
+            "param1": "val5",
+            "param2": "val2",
+            "param3": "val6",
+        }
+
+    mock_get_parameter = mocker.patch.object(
+        db, "get_model_parameters", side_effect=mock_get_parameters
+    )
+
+    # Mock logger to verify debug message
+    mock_logger = mocker.patch.object(db._logger, "debug")
+
+    result = db.get_model_parameters_for_all_model_versions(site, array_element_name, collection)
+
+    # Verify that the mocks were called correctly
+    mock_get_model_versions.assert_called_once_with(collection)
+    assert mock_get_parameter.call_count == 2  # Should still try both versions
+
+    # Verify debug message for skipped version
+    mock_logger.assert_called_once_with(
+        f"Skipping model version 5.0.0 - {array_element_name} not found"
+    )
+
+    # Verify result only contains data for version 6.0.0
+    expected = {
+        "6.0.0": {
+            "param1": "val5",
+            "param2": "val2",
+            "param3": "val6",
+        }
+    }
+
+    assert result == expected
+
+
 def test_get_model_parameters_with_cache(db, mocker, standard_test_params):
     """Test get_model_parameters method with cache."""
     site = standard_test_params["site"]
diff --git a/tests/unit_tests/reporting/test_docs_auto_report_generator.py b/tests/unit_tests/reporting/test_docs_auto_report_generator.py
new file mode 100644
index 0000000000..91515ad8c4
--- /dev/null
+++ b/tests/unit_tests/reporting/test_docs_auto_report_generator.py
@@ -0,0 +1,418 @@
+from pathlib import Path
+from unittest.mock import MagicMock, call, patch
+
+import pytest
+
+from simtools.reporting.docs_auto_report_generator import ReportGenerator
+
+GET_SITE_FROM_NAME_PATH = "simtools.utils.names.get_site_from_array_element_name"
+
+
+def test__add_design_models_to_telescopes(io_handler, db_config):
+    args = {"site": "North", "telescope": "LSTN-01"}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    telescopes = ["LSTN-01", "LSTN-02"]
+    model_version = "6.0.0"
+
+    # Mocking the design model responses
+    report_generator.db.get_design_model = MagicMock()
+    report_generator.db.get_design_model.side_effect = ["LSTN-design", None]
+
+    result = report_generator._add_design_models_to_telescopes(model_version, telescopes)
+
+    assert len(result) == 3  # Original telescopes + one design model
+    assert "LSTN-design" in result
+    assert "LSTN-01" in result
+    assert "LSTN-02" in result
+
+
+def test__filter_telescopes_by_site(io_handler, db_config):
+    args = {"site": "North", "telescope": "LSTN-01"}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    telescopes = ["LSTN-01", "LSTN-02", "MSTS-01", "MSTN-01"]
+    selected_sites = {"North"}
+
+    with patch(GET_SITE_FROM_NAME_PATH) as mock_get_site:
+        mock_get_site.side_effect = [["North"], ["North"], ["South"], ["North"]]
+        result = report_generator._filter_telescopes_by_site(telescopes, selected_sites)
+
+    assert len(result) == 3
+    assert "MSTS-01" not in result
+    assert all(tel in result for tel in ["LSTN-01", "LSTN-02", "MSTN-01"])
+
+
+@pytest.mark.parametrize(
+    "test_case",
+    [
+        # Case 1: All flags True - should return all combinations
+        {
+            "args": {
+                "all_telescopes": True,
+                "all_sites": True,
+                "all_model_versions": True,
+                "site": "North",
+                "telescope": "LSTN-01",
+                "model_version": "6.0.0",
+            },
+            "mock_model_versions": ["5.0.0", "6.0.0"],
+            "mock_array_elements": ["LSTN-01", "LSTN-02", "LSTN-design"],
+            "mock_sites": ["North"],
+            "expected_combinations": [
+                ("5.0.0", "LSTN-01", "North"),
+                ("5.0.0", "LSTN-02", "North"),
+                ("5.0.0", "LSTN-design", "North"),
+                ("6.0.0", "LSTN-01", "North"),
+                ("6.0.0", "LSTN-02", "North"),
+                ("6.0.0", "LSTN-design", "North"),
+            ],
+        },
+        # Case 2: Specific telescope, model version, and site
+        {
+            "args": {
+                "all_telescopes": False,
+                "all_sites": False,
+                "all_model_versions": False,
+                "site": "North",
+                "telescope": "LSTN-01",
+                "model_version": "6.0.0",
+            },
+            "mock_model_versions": ["6.0.0"],
+            "mock_array_elements": ["LSTN-01"],
+            "mock_sites": ["North"],
+            "expected_combinations": [("6.0.0", "LSTN-01", "North")],
+        },
+        # Case 3: All telescopes but specific site and version
+        {
+            "args": {
+                "all_telescopes": True,
+                "all_sites": False,
+                "all_model_versions": False,
+                "site": "North",
+                "telescope": "LSTN-01",
+                "model_version": "6.0.0",
+            },
+            "mock_model_versions": ["6.0.0"],
+            "mock_array_elements": ["LSTN-01", "LSTN-02", "LSTN-design"],
+            "mock_sites": ["North"],
+            "expected_combinations": [
+                ("6.0.0", "LSTN-01", "North"),
+                ("6.0.0", "LSTN-02", "North"),
+                ("6.0.0", "LSTN-design", "North"),
+            ],
+        },
+    ],
+)
+def test__generate_array_element_report_combinations(io_handler, db_config, test_case):
+    """Test array element report combinations generation with different flag combinations."""
+    report_generator = ReportGenerator(
+        db_config, test_case["args"], io_handler.get_output_directory()
+    )
+
+    with (
+        patch.multiple(
+            report_generator.db,
+            get_model_versions=MagicMock(return_value=test_case["mock_model_versions"]),
+            get_array_elements=MagicMock(return_value=test_case["mock_array_elements"]),
+        ),
+        patch(GET_SITE_FROM_NAME_PATH, return_value=test_case["mock_sites"]),
+    ):
+        result = list(report_generator._generate_array_element_report_combinations())
+
+        assert set(result) == set(test_case["expected_combinations"])
+
+        # Test that each combination is a tuple of 3 strings
+        for combo in result:
+            assert isinstance(combo, tuple)
+            assert len(combo) == 3
+            assert all(isinstance(item, str) for item in combo)
+
+        # Test specific values based on flags
+        if not test_case["args"]["all_model_versions"]:
+            assert all(combo[0] == test_case["args"]["model_version"] for combo in result)
+
+        if not test_case["args"]["all_telescopes"]:
+            assert all(combo[1] == test_case["args"]["telescope"] for combo in result)
+
+        if not test_case["args"]["all_sites"]:
+            assert all(combo[2] == test_case["args"]["site"] for combo in result)
+
+
+def test__get_telescopes_from_layout(io_handler, db_config):
+    """Test getting telescopes from layout for both all_telescopes=True and False cases."""
+    test_cases = [
+        # Test case 1: all_telescopes=True
+        {
+            "args": {"all_telescopes": True, "site": "North"},
+            "mock_layouts": {
+                "6.0.0": {
+                    "array_layouts": {
+                        "value": [
+                            {"name": "hyper_array", "elements": ["LSTN-01", "LSTN-02"]},
+                            {"name": "other_layout", "elements": ["MSTN-01"]},
+                        ]
+                    }
+                }
+            },
+            "mock_design_model": "LSTN-design",
+            "expected_telescopes": {"LSTN-01", "LSTN-02", "LSTN-design"},
+        },
+        # Test case 2: all_telescopes=False
+        {
+            "args": {"all_telescopes": False, "site": "North", "telescope": "LSTN-01"},
+            "mock_layouts": {},  # Not used in this case
+            "mock_design_model": None,  # Not used in this case
+            "expected_telescopes": {"LSTN-01"},
+        },
+    ]
+
+    for case in test_cases:
+        output_path = io_handler.get_output_directory()
+        report_generator = ReportGenerator(db_config, case["args"], output_path)
+
+        with patch.multiple(
+            report_generator.db,
+            get_model_parameters_for_all_model_versions=MagicMock(
+                return_value=case["mock_layouts"]
+            ),
+            get_design_model=MagicMock(return_value=case["mock_design_model"]),
+        ):
+            result = report_generator._get_telescopes_from_layout("North")
+
+            # Verify the result
+            assert result == case["expected_telescopes"]
+
+            # Verify DB methods were called correctly based on all_telescopes flag
+            if case["args"]["all_telescopes"]:
+                report_generator.db.get_model_parameters_for_all_model_versions.assert_called_once()
+                assert len(result) == len(case["expected_telescopes"])
+            else:
+                # Verify DB methods were not called when all_telescopes=False
+                report_generator.db.get_model_parameters_for_all_model_versions.assert_not_called()
+                report_generator.db.get_design_model.assert_not_called()
+                assert result == {case["args"]["telescope"]}
+
+
+def test__generate_parameter_report_combinations(io_handler, db_config):
+    """Test parameter report combinations generation for both specific and all telescopes."""
+
+    test_cases = [
+        # Test case 1: Specific telescope (all_telescopes=False)
+        {
+            "args": {"all_telescopes": False, "all_sites": True, "telescope": "LSTN-01"},
+            "mock_valid_sites": ["North"],
+            "expected_combinations": [("LSTN-01", "North")],
+        },
+        # Test case 2: All telescopes
+        {
+            "args": {"all_telescopes": True, "all_sites": True},
+            "mock_telescopes_north": {"LSTN-01", "LSTN-02", "LSTN-design"},
+            "mock_telescopes_south": {"MSTS-01", "MSTS-02"},
+            "expected_combinations": [
+                ("LSTN-01", "North"),
+                ("LSTN-02", "North"),
+                ("LSTN-design", "North"),
+                ("MSTS-01", "South"),
+                ("MSTS-02", "South"),
+            ],
+        },
+    ]
+
+    for case in test_cases:
+        output_path = io_handler.get_output_directory()
+        report_generator = ReportGenerator(db_config, case["args"], output_path)
+
+        with (
+            patch(
+                GET_SITE_FROM_NAME_PATH,
+                side_effect=lambda x, parent_case=case: ["North"]
+                if not parent_case["args"].get("all_telescopes")
+                else parent_case.get("mock_valid_sites", []),
+            ),
+            patch.multiple(
+                report_generator,
+                _get_telescopes_from_layout=MagicMock(
+                    side_effect=lambda site, parent_case=case: (
+                        parent_case.get("mock_telescopes_north", set())
+                        if site == "North"
+                        else parent_case.get("mock_telescopes_south", set())
+                    )
+                ),
+            ),
+        ):
+            result = list(report_generator._generate_parameter_report_combinations())
+            expected = case["expected_combinations"]
+
+            # Verify combinations
+            assert len(result) == len(expected)
+            assert set(result) == set(expected)
+
+            # For specific telescope case, verify _get_valid_sites_for_telescope was used
+            if not case["args"].get("all_telescopes"):
+                assert all(combo[0] == case["args"]["telescope"] for combo in result)
+
+
+def test_auto_generate_array_element_reports(io_handler, db_config):
+    args = {"all_telescopes": True, "all_sites": True, "all_model_versions": True}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    # Mock parameters that would be returned for different sites and telescopes
+    mock_params = [
+        ("6.0.0", "LSTN-01", "North"),
+        ("6.0.0", "LSTN-02", "North"),
+        ("6.0.0", "MSTN-01", "North"),
+        ("6.0.0", "MSTS-01", "South"),
+        ("6.0.0", "MSTS-02", "South"),
+        ("5.0.0", "LSTN-01", "North"),
+        ("5.0.0", "LSTN-02", "North"),
+        ("5.0.0", "MSTN-01", "North"),
+        ("5.0.0", "MSTS-01", "South"),
+        ("5.0.0", "MSTS-02", "South"),
+    ]
+
+    mock_report_gen = MagicMock()
+
+    with (
+        patch.object(
+            report_generator,
+            "_generate_array_element_report_combinations",
+            return_value=mock_params,
+        ),
+        patch.object(
+            report_generator, "_generate_single_array_element_report", side_effect=mock_report_gen
+        ),
+    ):
+        report_generator.auto_generate_array_element_reports()
+
+        # Verify that _generate_single_array_element_report was called for each combination
+        assert mock_report_gen.call_count == 10
+        mock_report_gen.assert_has_calls(
+            [
+                call("6.0.0", "LSTN-01", "North"),
+                call("6.0.0", "LSTN-02", "North"),
+                call("6.0.0", "MSTN-01", "North"),
+                call("6.0.0", "MSTS-01", "South"),
+                call("6.0.0", "MSTS-02", "South"),
+                call("5.0.0", "LSTN-01", "North"),
+                call("5.0.0", "LSTN-02", "North"),
+                call("5.0.0", "MSTN-01", "North"),
+                call("5.0.0", "MSTS-01", "South"),
+                call("5.0.0", "MSTS-02", "South"),
+            ],
+            any_order=True,
+        )
+
+
+def test_auto_generate_parameter_reports(io_handler, db_config):
+    """Test parameter report generation for multiple telescopes and sites."""
+    args = {"all_telescopes": True, "all_sites": True}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    # Mock telescope-site combinations that would be returned
+    mock_combinations = [
+        ("LSTN-01", "North"),
+        ("LSTN-02", "North"),
+        ("MSTS-01", "South"),
+        ("MSTS-02", "South"),
+    ]
+
+    with (
+        patch.object(
+            report_generator,
+            "_generate_parameter_report_combinations",
+            return_value=mock_combinations,
+        ),
+        patch(
+            "simtools.reporting.docs_read_parameters.ReadParameters.produce_model_parameter_reports",
+            new_callable=MagicMock,
+        ) as mock_produce,
+    ):
+        report_generator.auto_generate_parameter_reports()
+
+        # Verify that produce_model_parameter_reports was called for each combination
+        assert mock_produce.call_count == len(mock_combinations)
+
+        # Check that each telescope-site combination was processed with correct args
+        expected_calls = [call() for _ in mock_combinations]
+        mock_produce.assert_has_calls(expected_calls, any_order=True)
+
+
+def test__generate_single_array_element_report(io_handler, db_config):
+    """Test generation of a single array element report."""
+    # Initialize ReportGenerator with basic args
+    args = {"all_telescopes": True, "all_sites": True}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    # Test parameters
+    model_version = "6.0.0"
+    telescope = "LSTN-01"
+    site = "North"
+
+    # Mock ReadParameters and its produce_array_element_report method
+    mock_read_params = MagicMock()
+
+    with patch(
+        "simtools.reporting.docs_auto_report_generator.ReadParameters",
+        return_value=mock_read_params,
+    ) as mock_read_params_class:
+        # Call the method under test
+        report_generator._generate_single_array_element_report(model_version, telescope, site)
+
+        # Verify ReadParameters was instantiated with correct arguments
+        expected_args = {
+            "all_telescopes": True,
+            "all_sites": True,
+            "telescope": telescope,
+            "site": site,
+            "model_version": model_version,
+        }
+        expected_output_path = Path(output_path) / str(model_version)
+
+        mock_read_params_class.assert_called_once_with(
+            db_config, expected_args, expected_output_path
+        )
+
+        # Verify produce_array_element_report was called
+        mock_read_params.produce_array_element_report.assert_called_once()
+
+
+def test__get_valid_sites_for_telescope(io_handler, db_config):
+    """Test getting valid sites for different telescope types."""
+    args = {"all_telescopes": True, "all_sites": True}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    test_cases = [
+        # Test single site telescope (LST North)
+        {"telescope": "LSTN-01", "mock_return": "North", "expected": ["North"]},
+        # Test multi-site telescope (MST)
+        {
+            "telescope": "MST-FlashCam",
+            "mock_return": ["North", "South"],
+            "expected": ["North", "South"],
+        },
+        # Test South site telescope
+        {"telescope": "MSTS-01", "mock_return": "South", "expected": ["South"]},
+    ]
+
+    for case in test_cases:
+        with patch(
+            GET_SITE_FROM_NAME_PATH,
+            return_value=case["mock_return"],
+        ) as mock_get_site:
+            result = report_generator._get_valid_sites_for_telescope(case["telescope"])
+
+            # Verify get_site_from_array_element_name was called with correct telescope
+            mock_get_site.assert_called_once_with(case["telescope"])
+
+            # Verify returned sites match expected
+            assert result == case["expected"]
+
+            # Verify result is always a list
+            assert isinstance(result, list)
diff --git a/tests/unit_tests/reporting/test_docs_read_parameters.py b/tests/unit_tests/reporting/test_docs_read_parameters.py
index 6debbcff9f..1509fbefad 100644
--- a/tests/unit_tests/reporting/test_docs_read_parameters.py
+++ b/tests/unit_tests/reporting/test_docs_read_parameters.py
@@ -5,6 +5,9 @@
 
 from simtools.reporting.docs_read_parameters import ReadParameters
 
+# Test constants
+QE_FILE_NAME = "qe_lst1_20200318_high+low.dat"
+
 
 def test_get_all_parameter_descriptions(telescope_model_lst, io_handler, db_config):
     args = {
@@ -57,14 +60,12 @@ def test_produce_array_element_report(telescope_model_lst, io_handler, db_config
 
 def test_produce_model_parameter_reports(io_handler, db_config):
     args = {"site": "North", "telescope": "LSTN-01"}
-    output_path = io_handler.get_output_directory(
-        label="reports", sub_dir=f"parameters/{args['telescope']}"
-    )
+    output_path = io_handler.get_output_directory(label="reports", sub_dir="parameters")
     read_parameters = ReadParameters(db_config=db_config, args=args, output_path=output_path)
 
     read_parameters.produce_model_parameter_reports()
 
-    file_path = output_path / "quantum_efficiency.md"
+    file_path = output_path / args["telescope"] / "quantum_efficiency.md"
     assert file_path.exists()
 
 
@@ -190,7 +191,7 @@ def test__compare_parameter_across_versions(io_handler, db_config):
                 "instrument": "LSTN-01",
                 "site": "North",
                 "parameter_version": "1.0.0",
-                "value": "qe_lst1_20200318_high+low.dat",
+                "value": QE_FILE_NAME,
                 "unit": None,
                 "file": True,
             },
@@ -208,7 +209,7 @@ def test__compare_parameter_across_versions(io_handler, db_config):
                 "instrument": "LSTN-01",
                 "site": "North",
                 "parameter_version": "1.0.0",
-                "value": "qe_lst1_20200318_high+low.dat",
+                "value": QE_FILE_NAME,
                 "unit": None,
                 "file": True,
             },
@@ -256,3 +257,91 @@ def test__compare_parameter_across_versions(io_handler, db_config):
     assert position_comparison["parameter_version" == "2.0.0"]["model_version"] == "6.0.0"
 
     assert len(comparison_data.get("only_prod6_param")) == 1
+    assert "none_valued_param" not in comparison_data
+
+
+def test__compare_parameter_across_versions_empty_param_dict(io_handler, db_config):
+    """Test _compare_parameter_across_versions with empty parameter dictionaries."""
+    args = {"site": "North", "telescope": "LSTN-01"}
+    output_path = io_handler.get_output_directory(
+        label="reports", sub_dir=f"parameters/{args['telescope']}"
+    )
+    read_parameters = ReadParameters(db_config=db_config, args=args, output_path=output_path)
+
+    # Test data with empty parameter dictionary for version 5.0.0
+    mock_data = {
+        "5.0.0": {},  # Empty parameter dictionary
+        "6.0.0": {
+            "quantum_efficiency": {
+                "instrument": "LSTN-01",
+                "site": "North",
+                "parameter_version": "1.0.0",
+                "value": QE_FILE_NAME,
+                "unit": None,
+                "file": True,
+            }
+        },
+    }
+
+    comparison_data = read_parameters._compare_parameter_across_versions(
+        mock_data, ["quantum_efficiency"]
+    )
+
+    # Verify that data from version 6.0.0 is still processed
+    assert "quantum_efficiency" in comparison_data
+    qe_comparison = comparison_data["quantum_efficiency"]
+    assert len(qe_comparison) == 1
+    assert qe_comparison[0]["model_version"] == "6.0.0"
+    assert qe_comparison[0]["parameter_version"] == "1.0.0"
+
+
+def test_get_array_element_parameter_data_none_value(io_handler, db_config, mocker):
+    """Test that get_array_element_parameter_data correctly handles None values."""
+    args = {
+        "telescope": "tel",
+        "site": "North",
+        "model_version": "v1",
+    }
+    output_path = io_handler.get_output_directory(sub_dir=f"{args['model_version']}")
+    read_parameters = ReadParameters(db_config=db_config, args=args, output_path=output_path)
+
+    # Mock the model_parameters that will be filtered through
+    model_params = {"test_param": "description"}
+    mocker.patch("simtools.utils.names.model_parameters", return_value=model_params)
+
+    # Mock all_parameter_data with a None value
+    all_parameter_data = {
+        "test_param": {
+            "value": None,  # This will cause value_data to be None
+            "instrument": "tel",
+            "unit": "m",
+        }
+    }
+
+    # Mock the TelescopeModel
+    mock_telescope_model = mocker.MagicMock()
+    mock_telescope_model.name = "tel"
+    mock_telescope_model.site = args["site"]
+    mock_telescope_model.model_version = args["model_version"]
+
+    # Mock get_model_parameters to return our all_parameter_data
+    mocker.patch.object(read_parameters.db, "get_model_parameters", return_value=all_parameter_data)
+
+    # Mock get_all_parameter_descriptions
+    mocker.patch.object(
+        read_parameters,
+        "get_all_parameter_descriptions",
+        return_value=(
+            {"test_param": "Test parameter"},
+            {"test_param": "Test"},
+            {"test_param": "Structure"},
+        ),
+    )
+
+    result = read_parameters.get_array_element_parameter_data(mock_telescope_model)
+
+    # Assert result is empty (parameter was skipped due to None value)
+    assert len(result) == 0
+
+    # Verify the mocks were called
+    assert read_parameters.db.get_model_parameters.called


Reviews:
user: login: GernotMaier
body: Very nice! This looks good, a couple of minor comments below.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:03:31Z
user: login: EshitaJoshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T14:29:49Z
user: login: EshitaJoshi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T12:07:10Z
user: login: GernotMaier
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:24:15Z

Review comments:
diff_hunk: @@ -24,38 +26,55 @@ def _parse(label):
     )
 
     config.parser.add_argument(
-        "--parameter",
+        "--all_telescopes",
         action="store_true",
-        help="Compare all parameters across model versions for one telescope.",
+        help="Produce reports for all telescopes. Defaults to False.",
+    )
+
+    config.parser.add_argument(
+        "--all_model_versions",
+        action="store_true",
+        help="Produce reports for all model versions. Defaults to False.",
+    )
+
+    config.parser.add_argument(
+        "--all_sites", action="store_true", help="Produce reports for all sites. Defaults to False."
path: src/simtools/applications/docs_produce_array_element_report.py
user: login: GernotMaier
body: Note that the `Defaults to False.` is not needed. The help currently prints out duplication information

```
  --all_telescopes      Produce reports for all telescopes. Defaults to False. (default: False)
  --all_model_versions  Produce reports for all model versions. Defaults to False. (default: False)
  --all_sites           Produce reports for all sites. Defaults to False. (default: False)
```
created_at: 2025-03-21T12:49:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +24,16 @@ def _parse(label):
         description=("Produce a markdown report for model parameters."),
     )
 
+    config.parser.add_argument(
+        "--all_telescopes",
+        action="store_true",
+        help="Produce reports for all telescopes. Defaults to False.",
+    )
+
+    config.parser.add_argument(
+        "--all_sites", action="store_true", help="Produce reports for all sites. Defaults to False."
path: src/simtools/applications/docs_produce_model_parameter_reports.py
user: login: GernotMaier
body: Same comments on 'Defaults' is above.
created_at: 2025-03-21T13:02:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -290,10 +288,17 @@ def get_model_parameters_for_all_model_versions(self, site, array_element_name,
         """
         pars = defaultdict(dict)
         for _model_version in self.get_model_versions(collection):
-            parameter_data = self.get_model_parameters(
-                site, array_element_name, collection, _model_version
-            )
-            pars[_model_version].update(parameter_data)
+            try:
+                parameter_data = self.get_model_parameters(
+                    site, array_element_name, collection, _model_version
+                )
+                pars[_model_version].update(parameter_data)
+            except KeyError:
+                # Skip versions where array_element doesn't exist
path: src/simtools/db/db_handler.py
user: login: GernotMaier
body: No need for this comment - the debug message is saying exactly the same.
created_at: 2025-03-21T13:32:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,345 @@
+from pathlib import Path
+from unittest.mock import MagicMock, call, patch
+
+import pytest
+
+from simtools.reporting.docs_auto_report_generator import ReportGenerator
+
+GET_SITE_FROM_NAME_PATH = "simtools.utils.names.get_site_from_array_element_name"
+
+
+def test__add_design_models_to_telescopes(io_handler, db_config):
+    args = {"site": "North", "telescope": "LSTN-01"}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    telescopes = ["LSTN-01", "LSTN-02"]
+    model_version = "6.0.0"
+
+    # Mocking the design model responses
+    report_generator.db.get_design_model = MagicMock()
+    report_generator.db.get_design_model.side_effect = ["LSTN-design", None]
+
+    result = report_generator._add_design_models_to_telescopes(model_version, telescopes)
+
+    assert len(result) == 3  # Original telescopes + one design model
+    assert "LSTN-design" in result
+    assert "LSTN-01" in result
+    assert "LSTN-02" in result
+
+
+def test__filter_telescopes_by_site(io_handler, db_config):
+    args = {"site": "North", "telescope": "LSTN-01"}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    telescopes = ["LSTN-01", "LSTN-02", "MSTS-01", "MSTN-01"]
+    selected_sites = {"North"}
+
+    with patch(GET_SITE_FROM_NAME_PATH) as mock_get_site:
+        mock_get_site.side_effect = [["North"], ["North"], ["South"], ["North"]]
+        result = report_generator._filter_telescopes_by_site(telescopes, selected_sites)
+
+    assert len(result) == 3
+    assert "MSTS-01" not in result
+    assert all(tel in result for tel in ["LSTN-01", "LSTN-02", "MSTN-01"])
+
+
+@pytest.mark.parametrize(
+    ("all_flags", "expected_count"),
+    [
+        ({"all_telescopes": True, "all_sites": True, "all_model_versions": True}, 6),
+        ({"all_telescopes": False, "all_sites": False, "all_model_versions": False}, 1),
+    ],
+)
+def test__get_report_parameters(io_handler, db_config, all_flags, expected_count):
+    args = {"site": "North", "telescope": "LSTN-01", "model_version": "6.0.0", **all_flags}
+    output_path = io_handler.get_output_directory()
+    report_generator = ReportGenerator(db_config, args, output_path)
+
+    with patch.multiple(
+        report_generator.db,
+        get_model_versions=MagicMock(return_value=["5.0.0", "6.0.0"]),
+        get_array_elements=MagicMock(return_value=["LSTN-01", "LSTN-02", "LSTN-design"]),
+    ):
+        with patch(GET_SITE_FROM_NAME_PATH, return_value=["North"]):
+            result = list(report_generator._get_report_parameters())
+
+    assert len(result) == expected_count
+    for version, telescope, site in result:
path: tests/unit_tests/reporting/test_docs_auto_report_generator.py
user: login: GernotMaier
body: Wondering if this tests shouldn't be the other way around. Don't we want to be sure that "5.0.0" and "6.0.0." is in the result? Right now, you would be fine with result would only be "5.0.0"
created_at: 2025-03-21T13:42:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _get_report_parameters(self):
path: src/simtools/reporting/docs_auto_report_generator.py
user: login: GernotMaier
body: This is quite a complex function and hard to understand. My I ask you to write an a bit longer docstring explaining  what the function is generating?
created_at: 2025-03-21T13:44:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _get_report_parameters(self):
+        """Generate parameters for report generation."""
+        all_model_versions = self.db.get_model_versions()
+        all_sites = {"North", "South"}
path: src/simtools/reporting/docs_auto_report_generator.py
user: login: GernotMaier
body: No need to hardwire the site list. You can get it from `names.site_names()`
created_at: 2025-03-21T13:46:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _get_report_parameters(self):
+        """Generate parameters for report generation."""
+        all_model_versions = self.db.get_model_versions()
+        all_sites = {"North", "South"}
+
+        model_versions = (
+            all_model_versions
path: src/simtools/reporting/docs_auto_report_generator.py
user: login: GernotMaier
body: Suggest to replace `all_model_version` by the DB call `self.db.get_model_versions()`. This will avoid the call to the DB in cases the model version is given through the command line.
created_at: 2025-03-21T13:48:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _get_report_parameters(self):
+        """Generate parameters for report generation."""
+        all_model_versions = self.db.get_model_versions()
+        all_sites = {"North", "South"}
+
+        model_versions = (
+            all_model_versions
+            if self.args.get("all_model_versions")
+            else [self.args["model_version"]]
+        )
+        selected_sites = all_sites if self.args.get("all_sites") else {self.args["site"]}
+
+        def get_telescopes(model_version):
+            if not self.args.get("all_telescopes"):
+                return [self.args["telescope"]]
+            telescopes = self.db.get_array_elements(model_version)
+            all_telescopes = self._add_design_models_to_telescopes(model_version, telescopes)
+            return self._filter_telescopes_by_site(all_telescopes, selected_sites)
+
+        def get_valid_sites(telescope):
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            return [site for site in sites if site in selected_sites]
+
+        def generate_combinations():
+            for version in model_versions:
+                telescopes = get_telescopes(version)
+                for telescope in telescopes:
+                    for site in get_valid_sites(telescope):
+                        yield version, telescope, site
+
+        return generate_combinations()
+
+    def _generate_single_array_element_report(self, model_version, telescope, site):
+        """Generate a single report with given parameters."""
+        self.args.update(
+            {
+                "telescope": telescope,
+                "site": site,
+                "model_version": model_version,
+            }
+        )
+
+        output_path = Path(self.output_path) / str(model_version)
+        ReadParameters(self.db_config, self.args, output_path).produce_array_element_report()
+
+        logger.info(
+            f"Markdown report generated for {site} "
+            f"Telescope {telescope} (v{model_version}): {output_path}"
+        )
+
+    def auto_generate_array_element_reports(self):
+        """Generate all reports based on which --all_* flag is passed."""
+        for params in self._get_report_parameters():
+            self._generate_single_array_element_report(*params)
+
+    def _get_telescopes_from_layout(self, site: str) -> set[str]:
+        """Get unique telescopes for a given site across all versions."""
+        if not self.args.get("all_telescopes"):
+            return {self.args["telescope"]}
+
+        # Get layouts for all versions for this site
+        layouts = self.db.get_model_parameters_for_all_model_versions(
+            site=site, array_element_name=f"OBS-{site}", collection="sites"
+        )
+
+        # Collect all telescopes from hyper array layouts
+        all_telescopes = set()
+        for key, version_data in layouts.items():
+            layout = version_data.get("array_layouts", {}).get("value", [])
+            hyper_array = next(
+                (layout["elements"] for layout in layout if layout["name"] == "hyper_array"), []
+            )
+            all_telescopes.update(hyper_array)
+            all_telescopes.update(self._add_design_models_to_telescopes(key, list(all_telescopes)))
+
+        return all_telescopes
+
+    def _get_valid_sites_for_telescope(self, telescope: str) -> list[str]:
+        """Get valid sites for a given telescope."""
+        sites = names.get_site_from_array_element_name(telescope)
+        return [sites] if not isinstance(sites, list) else sites
+
+    def _generate_parameter_report_combinations(self) -> Generator[tuple[str, str], None, None]:
+        """Generate combinations of telescopes and sites for report generation."""
+        if not self.args.get("all_telescopes"):
+            # For a specific telescope, get its valid sites regardless of --all_sites
+            telescope = self.args["telescope"]
+            for site in self._get_valid_sites_for_telescope(telescope):
+                yield telescope, site
+        else:
+            # For --all_telescopes, use selected sites to get telescopes
+            all_sites = {"North", "South"}
path: src/simtools/reporting/docs_auto_report_generator.py
user: login: GernotMaier
body: Same as before - dont' hardwire values but use the function provided by names.
created_at: 2025-03-21T13:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -216,14 +137,15 @@ def _compare_parameter_across_versions(self, all_param_data, all_parameter_names
 
         # Iterate over each model version
         for version in all_versions:
-            Path(f"{self.output_path}/model").mkdir(parents=True, exist_ok=True)
+            parameter_dict = all_param_data.get(version, {})
path: src/simtools/reporting/docs_read_parameters.py
user: login: GernotMaier
body: No test needed for the lines below?
created_at: 2025-03-21T14:01:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+#!/usr/bin/python3
+
+r"""Class to automate generation of all reports according to command line inputs."""
+
+import logging
+from collections.abc import Generator
+from pathlib import Path
+
+from simtools.db import db_handler
+from simtools.reporting.docs_read_parameters import ReadParameters
+from simtools.utils import names
+
+logger = logging.getLogger()
+
+
+class ReportGenerator:
+    """Automate report generation."""
+
+    def __init__(self, db_config, args, output_path):
+        """Initialise class."""
+        self._logger = logging.getLogger(__name__)
+        self.db = db_handler.DatabaseHandler(mongo_db_config=db_config)
+        self.db_config = db_config
+        self.args = args
+        self.output_path = output_path
+
+    def _add_design_models_to_telescopes(self, model_version, telescopes):
+        """Add design models to the list of telescopes for a given model version."""
+        updated_telescopes = telescopes.copy()  # Copy original list to avoid modifying it directly
+        for telescope in telescopes:
+            design_model = self.db.get_design_model(model_version, telescope)  # Get design model
+            if design_model and design_model not in updated_telescopes:
+                updated_telescopes.append(design_model)  # Add design model if not already present
+        return updated_telescopes
+
+    def _filter_telescopes_by_site(self, telescopes, selected_sites):
+        """Filter telescopes by selected sites."""
+        filtered_telescopes = []
+        for telescope in telescopes:
+            sites = names.get_site_from_array_element_name(telescope)
+            sites = sites if isinstance(sites, list) else [sites]
+            if any(site in selected_sites for site in sites):
+                filtered_telescopes.append(telescope)
+        return filtered_telescopes
+
+    def _get_report_parameters(self):
path: src/simtools/reporting/docs_auto_report_generator.py
user: login: EshitaJoshi
body: Function renamed and added a docstring.
created_at: 2025-03-21T14:29:48Z
author_association: COLLABORATOR
diff_hunk: @@ -216,14 +137,15 @@ def _compare_parameter_across_versions(self, all_param_data, all_parameter_names
 
         # Iterate over each model version
         for version in all_versions:
-            Path(f"{self.output_path}/model").mkdir(parents=True, exist_ok=True)
+            parameter_dict = all_param_data.get(version, {})
path: src/simtools/reporting/docs_read_parameters.py
user: login: EshitaJoshi
body: Added tests, coverage is now 100%
created_at: 2025-03-24T12:07:10Z
author_association: COLLABORATOR

PR comments:
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-14T14:43:27Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 0.00% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/0.svg?sanitize=true) 0.00% Coverage (95.30% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-14T15:22:12Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 38.20% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true) 38.20% Coverage (95.30% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-17T13:05:33Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 39.70% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true) 39.70% Coverage (93.70% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-17T14:59:29Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 39.70% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true) 39.70% Coverage (93.70% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-17T15:36:37Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 38.20% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true) 38.20% Coverage (93.70% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-19T10:31:45Z
body: ![Failed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 33.00% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/25.svg?sanitize=true) 33.00% Coverage (95.20% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-20T14:46:44Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 4 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 4 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-20T15:02:03Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-20T15:17:25Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T10:05:00Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T10:06:03Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T10:27:57Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T10:58:04Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 96.10% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T13:13:16Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 98.40% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-21T13:13:20Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/90.svg?sanitize=true) 98.40% Coverage (95.90% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-24T12:07:31Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/100.svg?sanitize=true) 100.00% Coverage (96.00% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-24T12:08:22Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 1 Issue
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 1 Code Smell

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/100.svg?sanitize=true) 100.00% Coverage (96.00% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: ctao-dpps-sonarqube[bot]
created_at: 2025-03-24T12:16:49Z
body: ![Passed](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/QualityGateBadge/passed.svg?sanitize=true)

# Analysis Details
## 0 Issues
- ![Bug](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/bug.svg?sanitize=true) 0 Bugs
- ![Vulnerability](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 0 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/CoverageChart/100.svg?sanitize=true) 100.00% Coverage (96.00% Estimated after merge)
- ![Duplications](https://sonar-cta-dpps.zeuthen.desy.de/static/communityBranchPlugin/checks/Duplications/3.svg?sanitize=true) 0.00% Duplicated Code (0.00% Estimated after merge)

**Project ID:** gammasim_simtools_AY_ssha9WiFxsX-2oy_w

[View in SonarQube](https://sonar-cta-dpps.zeuthen.desy.de/dashboard?id=gammasim_simtools_AY_ssha9WiFxsX-2oy_w&pullRequest=1436)


author_association: NONE
user: login: EshitaJoshi
created_at: 2025-03-24T13:30:25Z
body: Thanks @GernotMaier!
author_association: COLLABORATOR