Source code for utils.names

"""Validation of names."""

import glob
import logging
import re
from functools import cache
from pathlib import Path

import yaml

_logger = logging.getLogger(__name__)

__all__ = [
    "get_site_from_array_element_name",
    "get_array_element_type_from_name",
    "generate_file_name",
    "layout_telescope_list_file_name",
    "sanitize_name",
    "simtel_single_mirror_list_file_name",
    "simtel_config_file_name",
    "validate_site_name",
    "validate_array_element_id_name",
    "validate_array_element_name",
]


@cache
def array_elements():
    """
    Load array elements from reference files and keep in cache.

    Returns
    -------
    dict
        Array elements.
    """
    base_path = Path(__file__).parent
    with open(base_path / "../schemas/array_elements.yml", encoding="utf-8") as file:
        return yaml.safe_load(file)["data"]


@cache
def site_names():
    """
    Site names from reference file.

    The list of sites is derived from the sites listed in the model parameter
    schema files. Return a dictionary for compatibility with the validation routines.

    Returns
    -------
    dict
        Site names.
    """
    _array_elements = array_elements()
    _sites = {entry["site"] for entry in _array_elements.values()}
    return {site: [site.lower()] for site in _sites}


@cache
def load_model_parameters(class_key_list):
    model_parameters = {}
    schema_files = glob.glob(str(Path(__file__).parent / "../schemas/model_parameters") + "/*.yml")
    for schema_file in schema_files:
        with open(schema_file, encoding="utf-8") as f:
            data = yaml.safe_load(f)
        try:
            if data["instrument"]["class"] in class_key_list:
                model_parameters[data["name"]] = data
        except KeyError:
            pass
    return model_parameters


def site_parameters():
    return load_model_parameters(class_key_list="Site")


def telescope_parameters():
    return load_model_parameters(class_key_list=("Structure", "Camera", "Telescope"))


[docs] def validate_array_element_id_name(name): """ Validate array element ID. Allowed IDs are - design (for design array elements or testing) - array element ID (e.g., 1, 5, 15) - test (for testing) Parameters ---------- name: str or int Array element ID name. Returns ------- str Validated array element ID (added leading zeros, e.g., 1 is converted to 01). Raises ------ ValueError If name is not valid. """ if isinstance(name, int) or name.isdigit(): return f"{int(name):02d}" if name.lower() in ("design", "test"): return str(name).lower() msg = f"Invalid array element ID name {name}" _logger.error(msg) raise ValueError(msg)
[docs] def validate_site_name(name): """ Validate site name. Parameters ---------- name: str Site name. Returns ------- str Validated name. """ return _validate_name(name, site_names())
def _validate_name(name, all_names): """ Validate name given the all_names options. For each key in all_names, a list of options is given. If name is in this list, the key name is returned. Parameters ---------- name: str Name to validate. all_names: dict Dictionary with valid names. Returns ------- str Validated name. Raises ------ ValueError If name is not valid. """ for key in all_names.keys(): if isinstance(all_names[key], list) and name.lower() in [ item.lower() for item in all_names[key] ]: return key if name.lower() == key.lower(): return key msg = f"Invalid name {name}" raise ValueError(msg) def validate_array_element_type(name): """ Validate array element type (e.g., LSTN, MSTN). Parameters ---------- name: str Array element type. Returns ------- str Validated name. """ return _validate_name(name, array_elements())
[docs] def validate_array_element_name(name): """ Validate array element name (e.g., MSTN-design, MSTN-01). Parameters ---------- name: str Array element name. Returns ------- str Validated name. """ try: _array_element_type, _array_element_id = name.split("-") except ValueError as exc: msg = f"Invalid name {name}" raise ValueError(msg) from exc return ( _validate_name(_array_element_type, array_elements()) + "-" + validate_array_element_id_name(_array_element_id) )
def get_array_element_name_from_type_site_id(array_element_type, site, array_element_id): """ Get array element name from type, site and ID. Parameters ---------- array_element_type: str Array element type. site: str Site name. array_element_id: str Array element ID. Returns ------- str Array element name. """ _short_site = validate_site_name(site)[0] _val_id = validate_array_element_id_name(array_element_id) return f"{array_element_type}{_short_site}-{_val_id}"
[docs] def get_array_element_type_from_name(name): """ Get array element type from name, e.g. "LSTN", "MSTN". Parameters ---------- name: str Array element name Returns ------- str Array element type. """ return _validate_name(name.split("-")[0], array_elements())
def get_list_of_array_element_types( array_element_class="telescopes", site=None, observatory="CTAO" ): """ Get list of array element types. Parameters ---------- array_element_class: str Array element class site: str Site name (e.g., South or North). Returns ------- list List of array element types. """ return [ key for key, value in array_elements().items() if value["collection"] == array_element_class and (site is None or value["site"] == site) and (observatory is None or value["observatory"] == observatory) ]
[docs] def get_site_from_array_element_name(name): """ Get site name from array element name. Parameters ---------- name: str Array element name. Returns ------- str Site name (South or North). """ return array_elements()[get_array_element_type_from_name(name)]["site"]
def get_collection_name_from_array_element_name(name): """ Get collection name (e.g., telescopes, calibration_devices, sites) of array element from name. Parameters ---------- name: str Array element name. Returns ------- str Collection name . """ try: return array_elements()[get_array_element_type_from_name(name)]["collection"] except ValueError: pass try: validate_site_name(name) return "sites" except ValueError as exc: raise ValueError(f"Invalid array element name {name}: {exc}") from exc def get_simulation_software_name_from_parameter_name( par_name, simulation_software="sim_telarray", search_telescope_parameters=True, search_site_parameters=True, ): """ Get the name used in the simulation software from the model parameter name. Name convention is expected to be defined in the schema. Returns the parameter name if no simulation software name is found. Parameters ---------- par_name: str Model parameter name. simulation_software: str Simulation software name. search_telescope_parameters: bool If True, telescope model parameters are included. search_site_parameters: bool If True, site model parameters are included. Returns ------- str Simtel parameter name. """ _parameter_names = {} if search_telescope_parameters: _parameter_names.update(telescope_parameters()) if search_site_parameters: _parameter_names.update(site_parameters()) try: _parameter = _parameter_names[par_name] except KeyError as err: _logger.error(f"Parameter {par_name} without schema definition") raise err try: for software in _parameter.get("simulation_software", []): if software.get("name") == simulation_software: return software.get("internal_parameter_name", par_name) except TypeError: # catches cases for which 'simulation_software' is None pass return None def get_parameter_name_from_simtel_name(simtel_name): """ Get the model parameter name from the simtel parameter name. Assumes that both names are equal if not defined otherwise in names.py. Parameters ---------- simtel_name: str Simtel parameter name. Returns ------- str Model parameter name. """ _parameters = {**telescope_parameters(), **site_parameters()} for par_name, par_info in _parameters.items(): try: for software in par_info["simulation_software"]: if ( software["name"] == "sim_telarray" and software["internal_parameter_name"] == simtel_name ): return par_name except (KeyError, TypeError): # catches cases for which 'simulation_software' is None pass return simtel_name
[docs] def simtel_config_file_name( site, model_version, array_name=None, telescope_model_name=None, label=None, extra_label=None, ): """ sim_telarray config file name for a telescope. Parameters ---------- site: str South or North. telescope_model_name: str LST-1, MST-FlashCam, ... model_version: str Version of the model. label: str Instance label. extra_label: str Extra label in case of multiple telescope config files. Returns ------- str File name. """ name = "CTA" name += f"-{array_name}" if array_name is not None else "" name += f"-{site}" name += f"-{telescope_model_name}" if telescope_model_name is not None else "" name += f"-{model_version}" name += f"_{label}" if label is not None else "" name += f"_{extra_label}" if extra_label is not None else "" name += ".cfg" return name
[docs] def simtel_single_mirror_list_file_name( site, telescope_model_name, model_version, mirror_number, label ): """ sim_telarray mirror list file with a single mirror. Parameters ---------- site: str South or North. telescope_model_name: str North-LST-1, South-MST-FlashCam, ... model_version: str Version of the model. mirror_number: int Mirror number. label: str Instance label. Returns ------- str File name. """ name = f"CTA-single-mirror-list-{site}-{telescope_model_name}-{model_version}" name += f"-mirror{mirror_number}" name += f"_{label}" if label is not None else "" name += ".dat" return name
[docs] def layout_telescope_list_file_name(name, label): """ File name for files required at the RayTracing class. Parameters ---------- name: str Name of the array. label: str Instance label. Returns ------- str File name. """ file_name = f"telescope_positions-{name}" file_name += f"_{label}" if label is not None else "" file_name += ".ecsv" return file_name
[docs] def generate_file_name( file_type, suffix, site, telescope_model_name, zenith_angle, azimuth_angle=None, off_axis_angle=None, source_distance=None, mirror_number=None, label=None, extra_label=None, ): """ Generate a file name for output, config, or plotting. Used e.g., to generate camera-efficiency and ray-tracing output files. Parameters ---------- file_type: str Type of file (e.g., config, output, plot) suffix: str File suffix site: str South or North. telescope_model_name: str LSTN-01, MSTS-01, ... zenith_angle: float Zenith angle (deg). azimuth_angle: float Azimuth angle (deg). off_axis_angle: float Off-axis angle (deg). source_distance: float Source distance (km). mirror_number: int Mirror number. label: str Instance label. extra_label: str Extra label. Returns ------- str File name. """ name = f"{file_type}-{site}-{telescope_model_name}" name += f"-d{source_distance:.1f}km" if source_distance is not None else "" name += f"-za{float(zenith_angle):.1f}deg" name += f"-off{off_axis_angle:.3f}deg" if off_axis_angle is not None else "" name += f"_azm{round(azimuth_angle):03}deg" if azimuth_angle is not None else "" name += f"_mirror{mirror_number}" if mirror_number is not None else "" name += f"_{label}" if label is not None else "" name += f"_{extra_label}" if extra_label is not None else "" name += f"{suffix}" return name
[docs] def sanitize_name(name): """ Sanitize name to be a valid Python identifier. - Replaces spaces with underscores - Converts to lowercase - Removes characters that are not alphanumerics or underscores - If the name starts with a number, prepend an underscore Parameters ---------- name: str name to be sanitized. Returns ------- str: Sanitized name. Raises ------ ValueError: if the string name can not be sanitized. """ # Convert to lowercase sanitized = name.lower() # Replace spaces with underscores sanitized = sanitized.replace(" ", "_") # Remove characters that are not alphanumerics or underscores sanitized = re.sub(r"\W|^(?=\d)", "_", sanitized) if not sanitized.isidentifier(): msg = f"The string {name} could not be sanitized." _logger.error(msg) raise ValueError(msg) return sanitized