Source code for utils.names

"""Name utilities for array elements, sites, and model parameters.

Naming in simtools:

* 'site': South or North
* 'array element': e.g., LSTN-01, MSTN-01, ...
* 'array element type': e.g., LSTN, MSTN, ...
* 'array element ID': e.g., 01, 02, ...
* 'array element design type': e.g., design, test
* 'instrument class key': e.g., telescope, camera, structure
* 'model repository collection': e.g., telescopes, sites, calibration_devices

"""

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

import yaml

from simtools.constants import (
    RESOURCE_PATH,
    SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH,
)

_logger = logging.getLogger(__name__)


[docs] @cache def array_elements(): """ Get array elements and their properties. Returns ------- dict Array elements. """ # for efficiency reason, no functions from simtools.utils.general are used here with open(Path(RESOURCE_PATH) / "array_elements.yml", encoding="utf-8") as file: return yaml.safe_load(file)["data"]
[docs] @cache def array_element_common_identifiers(): """ Get array element IDs from CTAO common identifier. Returns ------- dict, dict Dictionary mapping array element names to their IDs and vice versa. """ # for efficiency reason, no functions from simtools.utils.general are used here id_to_name = {} with open(Path(RESOURCE_PATH) / "array-element-ids.json", encoding="utf-8") as file: data = json.load(file) id_to_name = {e["id"]: e["name"] for e in data["array_elements"]} name_to_id = {e["name"]: e["id"] for e in data["array_elements"]} return id_to_name, name_to_id
[docs] @cache def site_names(): """ Get site names. The list of sites is derived from the sites listed in array element definition file. Return a dictionary for compatibility with the validation '_validate_name' routine. Returns ------- dict Site names. """ return { site: [site.lower()] for entry in array_elements().values() for site in (entry["site"] if isinstance(entry["site"], list) else [entry["site"]]) }
[docs] @cache def array_element_design_types(array_element_type): """ Get array element design type (e.g., 'design' or 'flashcam'). Default values are ['design', 'test']. Parameters ---------- array_element_type Array element type Returns ------- list Array element design types. """ default_types = ["design", "test"] if array_element_type is None: return default_types try: return array_elements()[array_element_type].get("design_types", default_types) except KeyError as exc: raise ValueError(f"Invalid name {array_element_type}") from exc
[docs] def is_design_type(array_element_name): """ Check if array element is a design type (e.g., "MSTS-FlashCam" or "LSTN-design"). Parameters ---------- array_element_name: str Array element name. Returns ------- bool True if array element is a design type. """ return get_array_element_id_from_name(array_element_name) in array_element_design_types( get_array_element_type_from_name(array_element_name) )
[docs] def matches_array_element_name_or_design_type(element_name, reference_element_name): """ Check whether an array element name matches a reference exactly or by design type equivalence. Parameters ---------- element_name: str Candidate element name to validate and compare (e.g., "MSTN-01" or "MSTx-FlashCam"). reference_element_name: str Reference array element name used for comparison (e.g., "MSTN-01" or "MSTN-FlashCam"). Returns ------- bool True if both names are identical, or if they belong to the same design type. """ if element_name is None or reference_element_name is None: return False try: validated_element_name = validate_array_element_name(element_name) validated_reference_element_name = validate_array_element_name(reference_element_name) except ValueError: return False if validated_element_name == validated_reference_element_name: return True if "-" not in validated_element_name or "-" not in validated_reference_element_name: return False return is_design_type(validated_element_name) and ( get_array_element_type_from_name(validated_element_name) == get_array_element_type_from_name(validated_reference_element_name) )
[docs] def validate_array_element_id_name(array_element_id, array_element_type=None): """ Validate array element ID. Allowed IDs are - design types (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. array_element_type: str Array element type (e.g., LSTN, MSTN). 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(array_element_id, int) or array_element_id.isdigit(): return f"{int(array_element_id):02d}" if array_element_id in array_element_design_types(array_element_type): return str(array_element_id) raise ValueError(f"Invalid array element ID name {array_element_id}")
[docs] def validate_site_name(site_name): """ Validate site name. Parameters ---------- site_name: str Site name. Returns ------- str Validated name. """ return _validate_name(site_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)
[docs] def validate_array_element_type(array_element_type): """ Validate array element type (e.g., LSTN, MSTN). Parameters ---------- array_element_type: str Array element type. Returns ------- str Validated name. """ return _validate_name(array_element_type, array_elements())
[docs] def validate_array_element_name(array_element_name): """ Validate array element name (e.g., MSTx-NectarCam, MSTN-01). Forgiving validation, is it allows also to give a site name (e.g., OBS-North). Parameters ---------- array_element_name: str Array element name. Returns ------- str Validated name. """ try: _array_element_type, _array_element_id = array_element_name.split("-") except ValueError as exc: msg = f"Invalid name {array_element_name}" raise ValueError(msg) from exc if _array_element_type == "OBS": return f"OBS-{validate_site_name(_array_element_id)}" return ( _validate_name(_array_element_type, array_elements()) + "-" + validate_array_element_id_name(_array_element_id, _array_element_type) )
[docs] def generate_array_element_name_from_type_site_id(array_element_type, site, array_element_id): """ Generate a new array element name from array element type, site, and array element 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, array_element_type) return f"{array_element_type}{_short_site}-{_val_id}"
[docs] def get_array_element_type_from_name(array_element_name): """ Get array element type from array element name (e.g "MSTN" from "MSTN-01"). For sites, return site name. Parameters ---------- array_element_name: str Array element name Returns ------- str Array element type. """ try: # e.g. instrument is 'North' as given for the site parameters return validate_site_name(array_element_name) except ValueError: # any other telescope or calibration device if array_element_name.startswith("OBS-"): return validate_site_name(array_element_name.split("-", maxsplit=1)[1]) return _validate_name(array_element_name.split("-")[0], array_elements())
[docs] def get_array_element_id_from_name(array_element_name): """ Get array element ID from array element name, (e.g. "01" from "MSTN-01"). Parameters ---------- array_element_name: str Array element name Returns ------- str Array element ID. """ try: return validate_array_element_id_name( array_element_name.split("-")[1], array_element_name.split("-")[0] ) except IndexError as exc: raise ValueError(f"Invalid name {array_element_name}") from exc
[docs] def get_common_identifier_from_array_element_name(array_element_name, default_return=None): """ Get numerical common identifier from array element name as used by CTAO. Common identifiers are numerical IDs used by the CTAO ACADA and DPPS systems. Parameters ---------- array_element_name: str Array element name (e.g. LSTN-01) Returns ------- int Common identifier. """ _, name_to_id = array_element_common_identifiers() try: return name_to_id[array_element_name] except KeyError as exc: if default_return is not None: return default_return raise ValueError(f"Unknown array element name {array_element_name}") from exc
[docs] def get_array_element_name_from_common_identifier(common_identifier): """ Get array element name from common identifier as used by CTAO. Common identifiers are numerical IDs used by the CTAO ACADA and DPPS systems. Parameters ---------- common_identifier: int Common identifier. Returns ------- str Array element name. """ id_to_name, _ = array_element_common_identifiers() try: return id_to_name[common_identifier] except KeyError as exc: raise ValueError(f"Unknown common identifier {common_identifier}") from exc
[docs] def normalize_array_element_identifier(array_element_identifier): """ Normalize an array element identifier to an array element name when possible. Parameters ---------- array_element_identifier : str or int Array element identifier. Can be a CTAO common numerical identifier (used by ACADA/DPPS) or already an array element name. Returns ------- str Normalized array element identifier. """ identifier_str = str(array_element_identifier).strip() if not identifier_str.lstrip("+-").isdigit(): return identifier_str common_identifier = int(identifier_str) try: return get_array_element_name_from_common_identifier(common_identifier) except ValueError: return identifier_str
[docs] def normalize_array_element_identifier_container(array_element_identifiers): """ Normalize a container of array element identifiers. Parameters ---------- array_element_identifiers : str or list or tuple or set or None Array element identifiers, possibly encoded as a JSON list string (for example the serialized content of an ECSV ``subtype: json`` field). Returns ------- list List of normalized array element identifiers. """ if array_element_identifiers is None: return [] normalized_input = array_element_identifiers if isinstance(array_element_identifiers, str): identifiers_str = array_element_identifiers.strip() if identifiers_str.startswith("[") or identifiers_str.endswith("]"): try: normalized_input = json.loads(identifiers_str) except json.JSONDecodeError: raise ValueError( "Invalid JSON list string for array element identifiers: " f"{array_element_identifiers}" ) from None if not isinstance(normalized_input, list): raise ValueError( "Invalid JSON list string for array element identifiers: " f"{array_element_identifiers}" ) else: normalized_input = [array_element_identifiers] elif not isinstance(array_element_identifiers, (list, tuple, set)): normalized_input = [array_element_identifiers] return [normalize_array_element_identifier(identifier) for identifier in normalized_input]
[docs] def get_list_of_array_element_types( array_element_class="telescopes", site=None, observatory="CTAO" ): """ Get list of array element types (e.g., ["LSTN", "MSTN"] for the Northern site). 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 sorted( [ 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(array_element_name): """ Get site name from array element name (e.g., "South" from "MSTS-01"). Parameters ---------- array_element_name: str Array element name. Returns ------- str, list Site name(s). """ if array_element_name is None: return None try: # e.g. instrument is 'North' as given for the site parameters if array_element_name.startswith("OBS"): return validate_site_name(array_element_name.split("-")[1]) return validate_site_name(array_element_name) except ValueError: # e.g. instrument is 'LSTN' as given for the array element types return array_elements()[get_array_element_type_from_name(array_element_name)]["site"]
[docs] def get_collection_name_from_array_element_name(array_element_name, array_elements_only=True): """ Get collection name (e.g., telescopes, calibration_devices) of an array element from its name. Parameters ---------- array_element_name: str Array element name (e.g. LSTN-01) array_elements_only: bool If True, only array elements are considered (e.g. "OBS-North" will raise a ValueError). Returns ------- str Collection name . Raises ------ ValueError If name is not a valid array element name. """ try: return array_elements()[get_array_element_type_from_name(array_element_name)]["collection"] except (ValueError, KeyError) as exc: if array_elements_only: raise ValueError(f"Invalid array element name {array_element_name}") from exc try: if array_element_name.startswith("OBS") or validate_site_name(array_element_name): return "sites" except ValueError: pass if array_element_name in { "configuration_sim_telarray", "configuration_corsika", "Dummy-Telescope", }: return array_element_name raise ValueError(f"Invalid array element name {array_element_name}")
[docs] def sim_telarray_config_file_name(site, array_name=None, telescope_model_name=None): """ sim_telarray configuration file name for an array or a telescope. Parameters ---------- site: str South or North. array_name: str Array name (e.g., CTAO-North-4-LSTs-1-MSTs) telescope_model_name: str Telescope name (e.g., LSTN-01, MSTS-05) Returns ------- str Configuration file name. """ name = "CTAO" if array_name: name += f"-{site}-{array_name}" if telescope_model_name: name += f"-{telescope_model_name}" name += ".cfg" # Telescope config files are included by the array config and parsed by sim_telarray # through a fixed-size getword buffer. if telescope_model_name is not None and len(name) > SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH: raise ValueError( f"Generated configuration file name {name} exceeds the maximum length of " f"{SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH} characters." ) 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 generate_file_name( file_type, suffix, site, telescope_model_name, zenith_angle, azimuth_angle=None, off_axis_angle=None, off_axis_x=None, off_axis_y=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). Use off_axis_x and off_axis_y for (x, y) offsets. off_axis_x: float X component of off-axis offset (deg). off_axis_y: float Y component of off-axis offset (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" # Handle both 1D (r) and 2D (x,y) off_axis_angle offsets. if off_axis_x is not None and off_axis_y is not None: name += f"_off_x{off_axis_x:+.3f}_y{off_axis_y:+.3f}deg" elif off_axis_angle is not None: name += f"_off{off_axis_angle:.3f}deg" 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 file_name_with_version(file_name, suffix): """ Return a file name including a semantic version with the correct suffix. Replaces 'Path.suffix()', which removes trailing numbers (and therefore version numbers). Parameters ---------- file_name: str File name. suffix: str File suffix. Returns ------- Path File name with version number. """ if file_name is None or suffix is None: return None file_name = str(file_name) if re.search(r"\d{1,8}\.\d{1,8}\.\d{1,8}\Z", file_name): return Path(file_name + suffix) return Path(file_name).with_suffix(suffix)
[docs] def is_two_mirror_telescope(telescope_model_name): """Return True for two-mirror telescopes (SST and SCT).""" tel_type = get_array_element_type_from_name(telescope_model_name) return "SST" in tel_type or "SCT" in tel_type