"""MC model of a telescope."""
import logging
from copy import deepcopy
import numpy as np
import simtools.utils.general as gen
from simtools.model.camera import Camera
from simtools.model.mirrors import Mirrors
from simtools.model.model_parameter import InvalidModelParameterError, ModelParameter
from simtools.model_repository.asset_names import get_simtel_table_file_name
from simtools.simulation.configuration import get_model_writer
[docs]
class TelescopeModel(ModelParameter):
"""
TelescopeModel represents the MC model of an individual telescope.
TelescopeModel contains parameter names and values for a specific telescope model.
Parameters
----------
site: str
Site name (e.g., South or North).
telescope_name: str
Telescope name (ex. LSTN-01, LSTN-design, ...).
model_version: str
Model version.
label: str, optional
Instance label.
overwrite_model_parameter_dict: dict, optional
Dictionary to overwrite model parameters from the model repository with provided values.
ignore_software_version: bool, optional
If True, ignore software version checks for deprecated parameters.
model_directory: pathlib.Path or str, optional
Directory for generated model assets and sim_telarray configuration files.
"""
def __init__(
self,
site,
telescope_name,
model_version,
label=None,
overwrite_model_parameter_dict=None,
ignore_software_version=False,
model_reader=None,
model_directory=None,
context=None,
):
"""Initialize TelescopeModel."""
super().__init__(
site=site,
array_element_name=telescope_name,
model_version=model_version,
label=label,
overwrite_model_parameter_dict=overwrite_model_parameter_dict,
ignore_software_version=ignore_software_version,
model_reader=model_reader,
model_directory=model_directory,
context=context,
)
self._logger = logging.getLogger(__name__)
self._mirrors = None
self._camera = None
@property
def mirrors(self):
"""Load the mirror information if the class instance hasn't done it yet."""
if self._mirrors is None:
self._load_mirrors()
return self._mirrors
@property
def camera(self):
"""Load the camera information if the class instance hasn't done it yet."""
if self._camera is None:
self._load_camera()
return self._camera
[docs]
def export_single_mirror_list_file(self, mirror_number: int, set_focal_length_to_zero: bool):
"""
Export a mirror list file with a single mirror in it.
Parameters
----------
mirror_number: int
Number index of the mirror.
set_focal_length_to_zero: bool
Set the focal length to zero if True.
"""
return get_model_writer(self, context=self.context).export_single_mirror_list_file(
mirror_number=mirror_number, set_focal_length_to_zero=set_focal_length_to_zero
)
[docs]
def get_single_mirror_list_file(
self, mirror_number: int, set_focal_length_to_zero: bool = False
):
"""
Get the path to the single mirror list file.
Parameters
----------
mirror_number: int
Mirror number.
set_focal_length_to_zero: bool
Flag to set the focal length to zero.
Returns
-------
Path
Path of the single mirror list file.
"""
return get_model_writer(self, context=self.context).get_single_mirror_list_file(
mirror_number, set_focal_length_to_zero
)
def _load_mirrors(self):
"""Load the attribute mirrors by creating a Mirrors object with the mirror list file."""
mirror_list_file_name = self.get_parameter_value("mirror_list")
self._logger.debug(f"Reading mirror list from {mirror_list_file_name}")
try:
mirror_list_file = gen.find_file(mirror_list_file_name, self.config_file_directory)
except FileNotFoundError:
mirror_list_file = gen.find_file(mirror_list_file_name, self.io_handler.model_path)
self._logger.warning(
"Mirror_list_file was not found in the config directory - "
"Using the one found in the model_path"
)
except TypeError as exc:
raise TypeError("Undefined mirror list") from exc
self._mirrors = Mirrors(mirror_list_file, parameters=self.parameters)
[docs]
def get_telescope_effective_focal_length(
self, unit: str = "m", return_focal_length_if_zero: bool = False
) -> float:
"""
Return effective focal length.
The function ensures backwards compatibility with older sim-telarray versions.
Parameters
----------
unit: str
Unit of the effective focal length. Default is 'm'.
return_focal_length_if_zero: bool
If True, return the focal length if the effective focal length is 0.
Returns
-------
float:
Effective focal length.
"""
try:
eff_focal_length = self.get_parameter_value_with_unit("effective_focal_length")[0]
except TypeError:
eff_focal_length = self.get_parameter_value_with_unit("effective_focal_length")
try:
eff_focal_length = eff_focal_length.to(unit).value
except AttributeError:
eff_focal_length = 0.0
if return_focal_length_if_zero and (
eff_focal_length is None or np.isclose(eff_focal_length, 0.0)
):
self._logger.warning("Using focal_length because effective_focal_length is 0")
return self.get_parameter_value_with_unit("focal_length").to(unit).value
return eff_focal_length
def _load_camera(self):
"""Load the camera from its validated component parameters."""
configuration = self._resolve_camera_components()
focal_length = self.get_telescope_effective_focal_length("cm", True)
self._camera = Camera.from_configuration(self.name, configuration, focal_length)
def _resolve_camera_components(self):
"""Resolve the independent camera component parameters."""
pixel_types = deepcopy(self.get_parameter_value("camera_pixel_types"))
for pixel_type in pixel_types:
angle_parameter = pixel_type.get("lightguide_angle_parameter")
if angle_parameter:
parameter_data = self.parameters.get(angle_parameter, {})
pixel_type["lightguide_angle_file"] = (
get_simtel_table_file_name(parameter_data)
or f"{angle_parameter}-{self.name}.dat"
)
wavelength_parameter = pixel_type.get("lightguide_wavelength_parameter")
if wavelength_parameter:
parameter_data = self.parameters.get(wavelength_parameter, {})
pixel_type["lightguide_wavelength_file"] = (
get_simtel_table_file_name(parameter_data)
or f"{wavelength_parameter}-{self.name}.dat"
)
return {
"rotate": self.get_parameter_value("camera_rotate"),
"pixel_types": pixel_types,
"pixels": self.model_reader.get_parameter_table_records(
self._get_parameter_dict("camera_pixel_layout")
),
"triggers": self.model_reader.get_parameter_table_records(
self._get_parameter_dict("camera_trigger_groups")
),
"trigger_members": self.model_reader.get_parameter_table_records(
self._get_parameter_dict("camera_trigger_members")
),
}
[docs]
def get_on_axis_eff_optical_area(self) -> float:
"""Return the on-axis effective optical area (derived previously for this telescope)."""
ray_tracing_data = self.get_parameter_table("optics_properties")
if not np.isclose(ray_tracing_data["off_axis_angle"][0], 0):
msg = (
f"No value for the on-axis effective optical area exists."
f" The minimum off-axis angle is {ray_tracing_data['off_axis_angle'][0]}"
)
raise ValueError(msg)
return ray_tracing_data["eff_area"][0]
[docs]
def position(self, coordinate_system: str = "ground") -> list:
"""
Get coordinates in the given system.
Parameters
----------
coordinate_system: str
Coordinates system. Default is 'ground'.
Returns
-------
list :
List of telescope position in the requested coordinate system.
Raises
------
KeyError
If the coordinate system is not found.
"""
try:
return self.get_parameter_value_with_unit(f"array_element_position_{coordinate_system}")
except InvalidModelParameterError as exc:
raise InvalidModelParameterError(
f"Coordinate system {coordinate_system} not found."
) from exc
[docs]
def get_calibration_device_name(self, device_type):
"""
Get the calibration device name for this telescope.
Parameters
----------
device_type: str
Type of the calibration device (e.g., 'flasher', 'illuminator')
Returns
-------
str or None
Calibration device name or None if not defined.
"""
try:
devices = self.get_parameter_value("calibration_devices") or {}
except InvalidModelParameterError:
return None
return devices.get(device_type)