Source code for plot_array_layout

#!/usr/bin/python3

"""
Plot array elements (array layout).

Plot an array layout and save it to file (e.g., pdf). Layouts are defined in the database,
or given as command line arguments (explicit listing or telescope list file). A list of input
files is also accepted.
Layouts can be plotted in ground or UTM coordinate systems.

Listing of array elements follows this logic:

* explicit listing: e.g., ``-array_element_list MSTN-01, MSTN05``
* listing of types: e.g, ``-array_element_list MSTN`` plots all telescopes of type MSTN.

A rotation angle in degrees allows to rotate the array before plotting.
The typical image formats (e.g., pdf, png, jpg) are allowed for the output figures.
If no ``figure_name`` is given as output, layouts are plotted in pdf and png format.

Example of a layout plot:

.. _plot_array_layout_plot:
.. image:: images/plot_array_layout_example.png
    :width: 49 %

Command line arguments
----------------------
figure_name : str
    File name for the output figure.
array_layout_file : str
    File (astropy table compatible) with a list of array elements.
array_layout_name : str
    Name of the layout array (e.g., test_layout, alpha, 4mst, etc.).
array_element_list : list
    List of array elements (e.g., telescopes) to plot (e.g., ``LSTN-01 LSTN-02 MSTN``).
coordinate_system : str, optional
    Coordinate system for the array layout (ground or utm).
rotate_angle : float, optional
    Angle to rotate the array before plotting (in degrees).
show_labels : bool, optional
    Shows the telescope labels in the plot.
axes_range : float, optional
    Range of the both axes in meters.
marker_scaling : float, optional.
    Scaling factor for plotting of array elements, optional.

Examples
--------
Plot layout with the name "test_layout":

.. code-block:: console

    simtools-plot-layout-array --figure_name northern_array_alpha
                               --array_layout_name test_layout


Plot layout with 2 LSTs and all northern MSTs in UTM coordinates:

.. code-block:: console

    simtools-plot-layout-array --array_element_list LSTN-01 LSTN-02 MSTN
                               --coordinate_system utm

Plot layout from a file with the list of telescopes:

.. code-block:: console

    simtools-plot-layout-array --array_element_list telescope_positions-test_layout.ecsv
"""

import logging
from pathlib import Path

import matplotlib as mpl
import matplotlib.pyplot as plt
from astropy import units as u

import simtools.utils.general as gen
from simtools.configuration import configurator
from simtools.io_operations import io_handler
from simtools.model.array_model import ArrayModel
from simtools.utils import names
from simtools.visualization.visualize import plot_array


def _parse(label, description, usage):
    """
    Parse command line configuration.

    Parameters
    ----------
    label : str
        Label describing the application.
    description : str
        Description of the application.
    usage : str
        Example on how to use the application.

    Returns
    -------
    CommandLineParser
        Command line parser object.
    """
    config = configurator.Configurator(label=label, description=description, usage=usage)

    config.parser.add_argument(
        "--figure_name",
        help="Name of the output figure to be saved into as a pdf.",
        type=str,
        required=False,
        default=None,
    )
    config.parser.add_argument(
        "--rotate_angle",
        help="Angle to rotate the array (in degrees).",
        type=str,
        required=False,
        default=None,
    )
    config.parser.add_argument(
        "--show_labels",
        help="Plot array element labels.",
        action="store_true",
        required=False,
        default=False,
    )
    config.parser.add_argument(
        "--marker_scaling",
        help="Scaling factor for the markers.",
        type=float,
        required=False,
        default=1.0,
    )
    config.parser.add_argument(
        "--coordinate_system",
        help="Coordinate system for the array layout.",
        type=str,
        required=False,
        default="ground",
        choices=["ground", "utm"],
    )
    config.parser.add_argument(
        "--axes_range",
        help="Range of the both axes in meters.",
        type=float,
        required=False,
        default=None,
    )
    return config.initialize(db_config=True, simulation_model=["site", "layout", "layout_file"])


def _get_site_from_telescope_list_name(telescope_list_file):
    """
    Get the site name from the telescope list file name.

    Parameters
    ----------
    telescope_list_file : str
        Telescope list file name.

    Returns
    -------
    str
        Site name.
    """
    for _site in names.site_names():
        if _site in str(telescope_list_file):
            return _site
    return None


def _get_list_of_plot_files(plot_file_name, output_dir):
    """
    Get list of output file names for plotting.

    Parameters
    ----------
    plot_file_name : str
        Name of the plot file.
    output_dir : str
        Output directory.

    Returns
    -------
    list
        List of output file names.

    Raises
    ------
    NameError
        If the file extension is not valid.
    """
    plot_file = output_dir.joinpath(plot_file_name)

    if len(plot_file.suffix) == 0:
        return [plot_file.with_suffix(f".{ext}") for ext in ["pdf", "png"]]

    allowed_extensions = [".jpeg", ".jpg", ".png", ".tiff", ".ps", ".pdf", ".bmp"]
    if plot_file.suffix in allowed_extensions:
        return [plot_file]
    msg = f"Extension in {plot_file} is not valid. Valid extensions are: {allowed_extensions}."
    raise NameError(msg)


def _get_plot_file_name(figure_name, layout_name, site, coordinate_system, rotate_angle):
    """
    Generate and return the file name for plots.

    Parameters
    ----------
    figure_name : str
        Figure name given through command line.
    layout_name : str
        Name of the layout.
    site : str
        Site name.
    coordinate_system : str
        Coordinate system for the array layout.
    rotate_angle : float
        Angle to rotate the array before plotting.

    Returns
    -------
    str
        Plot file name.
    """
    if figure_name is not None:
        return figure_name

    return (
        f"array_layout_{layout_name}_{site}_{coordinate_system}_"
        f"{round(rotate_angle.to(u.deg).value)!s}deg"
    )


def _layouts_from_array_layout_file(args_dict, db_config, rotate_angle):
    """
    Read array layout positions from file(s) and return a list of layouts.

    Parameters
    ----------
    args_dict : dict
        Dictionary with the command line arguments.
    db_config : dict
        Database configuration.
    rotate_angle : float
        Angle to rotate the array before plotting (in degrees).

    Returns
    -------
    list
        List of array layouts.
    """
    layouts = []
    telescope_files = args_dict["array_layout_file"]
    for one_file in telescope_files:
        site = (
            _get_site_from_telescope_list_name(one_file)
            if args_dict["site"] is None
            else args_dict["site"]
        )
        array_model = ArrayModel(
            mongo_db_config=db_config,
            model_version=args_dict["model_version"],
            site=site,
            array_elements=one_file,
        )
        layouts.append(
            {
                "array_elements": array_model.export_array_elements_as_table(),
                "plot_file_name": _get_plot_file_name(
                    args_dict["figure_name"],
                    (Path(one_file).name).split(".")[0],
                    site,
                    args_dict["coordinate_system"],
                    rotate_angle,
                ),
            }
        )
    return layouts


def _layouts_from_list(args_dict, db_config, rotate_angle):
    """
    Read positions for a list of array elements from the database and return a list of layouts.

    Parameters
    ----------
    args_dict : dict
        Dictionary with the command line arguments.
    db_config : dict
        Database configuration.
    rotate_angle : float
        Angle to rotate the array before plotting (in degrees).

    Returns
    -------
    list
        List of array layouts.
    """
    site = (
        names.get_site_from_array_element_name(args_dict["array_element_list"][0])
        if args_dict["site"] is None
        else args_dict["site"]
    )
    array_model = ArrayModel(
        mongo_db_config=db_config,
        model_version=args_dict["model_version"],
        site=site,
        array_elements=args_dict["array_element_list"],
    )
    return [
        {
            "array_elements": array_model.export_array_elements_as_table(
                coordinate_system=args_dict["coordinate_system"]
            ),
            "plot_file_name": _get_plot_file_name(
                args_dict["figure_name"],
                "list",
                site,
                args_dict["coordinate_system"],
                rotate_angle,
            ),
        }
    ]


def _layouts_from_db(args_dict, db_config, rotate_angle):
    """
    Read array elements and their positions from data base using the layout name.

    Parameters
    ----------
    args_dict : dict
        Dictionary with the command line arguments.
    db_config : dict
        Database configuration.
    rotate_angle : float
        Angle to rotate the array before plotting (in degrees).

    Returns
    -------
    list
        List of array layouts.
    """
    layouts = []
    array_model = ArrayModel(
        mongo_db_config=db_config,
        model_version=args_dict["model_version"],
        site=args_dict["site"],
        layout_name=args_dict["array_layout_name"],
    )
    layouts.append(
        {
            "array_elements": array_model.export_array_elements_as_table(
                coordinate_system=args_dict["coordinate_system"]
            ),
            "plot_file_name": _get_plot_file_name(
                figure_name=args_dict["figure_name"],
                layout_name=args_dict["array_layout_name"],
                site=args_dict["site"],
                coordinate_system=args_dict["coordinate_system"],
                rotate_angle=rotate_angle,
            ),
        }
    )
    return layouts


[docs] def main(): """Plot array layout application.""" label = Path(__file__).stem args_dict, db_config = _parse( label, "Plots array layout.", "python applications/plot_array_layout.py --array_layout_name test_layout", ) logger = logging.getLogger() logger.setLevel(gen.get_log_level_from_user(args_dict["log_level"])) io_handler_instance = io_handler.IOHandler() rotate_angle = ( 0.0 * u.deg if args_dict["rotate_angle"] is None else float(args_dict["rotate_angle"]) * u.deg ) layouts = [] if args_dict["array_layout_name"] is not None: logger.info("Plotting array from layout array name.") layouts = _layouts_from_db(args_dict, db_config, rotate_angle) elif args_dict["array_layout_file"] is not None: logger.info("Plotting array from telescope list file.") layouts = _layouts_from_array_layout_file(args_dict, db_config, rotate_angle) elif args_dict["array_element_list"] is not None: logger.info("Plotting array from list of array elements.") layouts = _layouts_from_list(args_dict, db_config, rotate_angle) mpl.use("Agg") for layout in layouts: fig_out = plot_array( telescopes=layout["array_elements"], rotate_angle=rotate_angle, show_tel_label=args_dict["show_labels"], axes_range=args_dict["axes_range"], marker_scaling=args_dict["marker_scaling"], ) _plot_files = _get_list_of_plot_files( layout["plot_file_name"], io_handler_instance.get_output_directory(label, sub_dir="application-plots"), ) for file in _plot_files: logger.info(f"Saving figure as {file}") plt.savefig(file, bbox_inches="tight", dpi=400) fig_out.clf() plt.close()
if __name__ == "__main__": main()