Source code for simtel.simulator_ray_tracing

"""Simulation runner for ray tracing simulations."""

import logging
from collections import namedtuple

import astropy.units as u

from simtools.io_operations import io_handler
from simtools.runners.simtel_runner import SimtelRunner
from simtools.utils import names

__all__ = ["SimulatorRayTracing"]

# pylint: disable=no-member
# The line above is needed because there are members which are created
# by adding them to the __dict__ of the class rather than directly.


[docs] class SimulatorRayTracing(SimtelRunner): """ Perform ray tracing simulations with sim_telarray. Parameters ---------- telescope_model: TelescopeModel telescope model label: str label used for output file naming. simtel_path: str or Path Location of sim_telarray installation. config_data: namedtuple namedtuple containing the configurable parameters as values (expected units in brackets): zenith_angle (deg), off_axis_angle (deg), source_distance (km), single_mirror_mode, use_random_focal_length, mirror_numbers. force_simulate: bool Remove existing files and force re-running of the ray-tracing simulation. """ def __init__( self, telescope_model, label=None, simtel_path=None, config_data=None, force_simulate=False, test=False, ): """Initialize SimtelRunner.""" self._logger = logging.getLogger(__name__) self._logger.debug("Init SimulatorRayTracing") super().__init__(label=label, simtel_path=simtel_path) self.telescope_model = telescope_model self.label = label if label is not None else self.telescope_model.label self.io_handler = io_handler.IOHandler() self._base_directory = self.io_handler.get_output_directory(self.label, "ray-tracing") self.config = ( self._config_to_namedtuple(config_data) if isinstance(config_data, dict) else config_data ) self._rep_number = 0 self.runs_per_set = 1 if self.config.single_mirror_mode else 20 self.photons_per_run = 100000 if not test else 5000 self._load_required_files(force_simulate) def _load_required_files(self, force_simulate): """ Load required files for the simulation. Depends on the running mode. Initialize files for the simulation. Parameters ---------- force_simulate: bool Remove existing files and force re-running of the ray-tracing simulation. """ # This file is not actually needed and does not exist in simtools. # It is required as CORSIKA input file to sim_telarray self._corsika_file = self._simtel_path.joinpath("run9991.corsika.gz") # Loop to define and remove existing files. # Files will be named _base_file = self.__dict__['_' + base + 'File'] for base_name in ["stars", "photons", "log"]: file_name = names.generate_file_name( file_type=base_name, suffix=".log" if base_name == "log" else ".lis", site=self.telescope_model.site, telescope_model_name=self.telescope_model.name, source_distance=( None if self.config.single_mirror_mode else self.config.source_distance ), zenith_angle=self.config.zenith_angle, off_axis_angle=self.config.off_axis_angle, mirror_number=( self.config.mirror_numbers if self.config.single_mirror_mode else None ), label=self.label, ) file = self._base_directory.joinpath(file_name) if file.exists() and force_simulate: file.unlink() # Defining the file name variable as an class attribute. self.__dict__["_" + base_name + "_file"] = file if not file.exists() or force_simulate: # Adding header to photon list file. with self._photons_file.open("w", encoding="utf-8") as file: file.write(f"#{50 * '='}\n") file.write("# List of photons for RayTracing simulations\n") file.write(f"#{50 * '='}\n") file.write(f"# config_file = {self.telescope_model.get_config_file()}\n") file.write(f"# zenith_angle [deg] = {self.config.zenith_angle}\n") file.write(f"# off_axis_angle [deg] = {self.config.off_axis_angle}\n") file.write(f"# source_distance [km] = {self.config.source_distance}\n") if self.config.single_mirror_mode: file.write(f"# mirror_number = {self.config.mirror_numbers}\n\n") # Filling a star file with a single light source defined by # - azimuth # - elevation # - flux # - distance of light source with self._stars_file.open("w", encoding="utf-8") as file: file.write( f"0. {90.0 - self.config.zenith_angle} 1.0 {self.config.source_distance}\n" ) if self.config.single_mirror_mode: self._logger.debug("For single mirror mode, need to prepare the single pixel camera.") self._write_out_single_pixel_camera_file() def _make_run_command( self, run_number=None, input_file=None ): # pylint: disable=unused-argument """Generate simtel_array run command.""" if self.config.single_mirror_mode: # Note: no mirror length defined for dual-mirror telescopes _mirror_focal_length = float( self.telescope_model.get_parameter_value("mirror_focal_length") ) # RayTracing command = str(self._simtel_path.joinpath("sim_telarray/bin/sim_telarray")) command += f" -c {self.telescope_model.get_config_file()}" command += f" -I{self.telescope_model.config_file_directory}" command += super().get_config_option("random_state", "none") command += super().get_config_option("IMAGING_LIST", str(self._photons_file)) command += super().get_config_option("stars", str(self._stars_file)) command += super().get_config_option( "altitude", self.telescope_model.get_parameter_value("corsika_observation_level") ) command += super().get_config_option( "telescope_theta", self.config.zenith_angle + self.config.off_axis_angle, ) command += super().get_config_option("star_photons", str(self.photons_per_run)) command += super().get_config_option("telescope_phi", "0") command += super().get_config_option("camera_transmission", "1.0") command += super().get_config_option("nightsky_background", "all:0.") command += super().get_config_option("trigger_current_limit", "1e10") command += super().get_config_option("telescope_random_angle", "0") command += super().get_config_option("telescope_random_error", "0") command += super().get_config_option("convergent_depth", "0") command += super().get_config_option("maximum_telescopes", "1") command += super().get_config_option("show", "all") command += super().get_config_option("camera_filter", "none") if self.config.single_mirror_mode: command += super().get_config_option("focus_offset", "all:0.") command += super().get_config_option("camera_config_file", "single_pixel_camera.dat") command += super().get_config_option("camera_pixels", "1") command += super().get_config_option("trigger_pixels", "1") command += super().get_config_option("camera_body_diameter", "0") command += super().get_config_option( "mirror_list", self.telescope_model.get_single_mirror_list_file( self.config.mirror_numbers, self.config.use_random_focal_length ), ) command += super().get_config_option( "focal_length", self.config.source_distance * u.km.to(u.cm) ) command += super().get_config_option("dish_shape_length", _mirror_focal_length) command += super().get_config_option("mirror_focal_length", _mirror_focal_length) command += super().get_config_option("parabolic_dish", "0") command += super().get_config_option("mirror_align_random_distance", "0.") command += super().get_config_option("mirror_align_random_vertical", "0.,28.,0.,0.") command += " " + str(self._corsika_file) command += f" 2>&1 > {self._log_file} 2>&1" return command def _check_run_result(self, run_number=None): # pylint: disable=unused-argument """ Check run results. Photon list files should have at least 100 lines. Returns ------- bool True if photon list is not empty. Raises ------ RuntimeError if Photon list is empty. """ with open(self._photons_file, "rb") as ff: n_lines = sum(1 for _ in ff) if n_lines < 100: raise RuntimeError("Photon list is empty.") return True def _write_out_single_pixel_camera_file(self): """Write out the single pixel camera file.""" with self.telescope_model.config_file_directory.joinpath("single_pixel_camera.dat").open( "w" ) as file: file.write("# Single pixel camera\n") file.write('PixType 1 0 0 300 1 300 0.00 "funnel_perfect.dat"\n') file.write("Pixel 0 1 0. 0. 0 0 0 0x00 1\n") file.write("Trigger 1 of 0\n") # need to also write out the funnel_perfect.dat file with self.telescope_model.config_file_directory.joinpath("funnel_perfect.dat").open( "w" ) as file: file.write( "# Perfect light collection where the angular efficiency of funnels is needed\n" ) file.write("0 1.0\n") file.write("30 1.0\n") file.write("60 1.0\n") file.write("90 1.0\n") def _config_to_namedtuple(self, data_dict): """Convert dict to namedtuple for configuration.""" config_data = namedtuple( "Config", [ "zenith_angle", "off_axis_angle", "source_distance", "single_mirror_mode", "use_random_focal_length", "mirror_numbers", ], ) return config_data( zenith_angle=data_dict["zenith_angle"], off_axis_angle=data_dict["off_axis_angle"], source_distance=data_dict["source_distance"], single_mirror_mode=data_dict["single_mirror_mode"], use_random_focal_length=data_dict["use_random_focal_length"], mirror_numbers=data_dict["mirror_numbers"], )