"""Simulation runner for ray tracing simulations."""
import logging
import astropy.units as u
import simtools.utils.general as gen
from simtools.io_operations import io_handler
from simtools.runners.simtel_runner import SimtelRunner
from simtools.utils import names, value_conversion
__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):
"""
SimulatorRayTracing is the interface with sim_telarray to perform ray tracing simulations.
Configurable parameters:
zenith_angle:
len: 1
unit: deg
default: 20 deg
off_axis_angle:
len: 1
unit: deg
default: 0 deg
source_distance:
len: 1
unit: km
default: 10 km
single_mirror_mode:
len: 1
default: False
use_random_focal_length:
len: 1
default: False
mirror_numbers:
len: 1
default: 1
Parameters
----------
telescope_model: str
Instance of TelescopeModel class.
label: str
Instance label. Important for output file naming.
simtel_path: str or Path
Location of sim_telarray installation.
config_data: dict
Dict containing the configurable parameters.
config_file: str or Path
Path of the yaml file containing the configurable parameters.
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,
config_file=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")
# Loading config_data
self.config = value_conversion.validate_config_data(
gen.collect_data_from_file_or_dict(config_file, config_data),
self.ray_tracing_default_configuration(True),
)
# RayTracing - default parameters
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):
"""
Which file are required for running depends on the mode.
Here we define and write some information into these files. Log files are always required.
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.
# However, we need to provide the name of a CORSIKA input file to sim_telarray
# so it is set up here.
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=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 in star file with a single light source.
# Parameters defining light source:
# - 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
"""Return the command to run simtel_array."""
if self.config.single_mirror_mode:
_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 += " 2>&1 > " + str(self._log_file) + " 2>&1"
return command
def _check_run_result(self, run_number=None): # pylint: disable=unused-argument
"""Check run results.
Raises
------
RuntimeError
if Photon list is empty.
"""
# Checking run
if not self._is_photon_list_file_ok():
msg = "Photon list is empty."
self._logger.error(msg)
raise RuntimeError(msg)
self._logger.debug("Everything looks fine with output file.")
def _is_photon_list_file_ok(self):
"""Check if the photon list is valid."""
n_lines = 0
with open(self._photons_file, "rb") as ff:
for _ in ff:
n_lines += 1
if n_lines > 100:
break
return n_lines > 100
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")
[docs]
@staticmethod
def ray_tracing_default_configuration(config_runner=False):
"""
Get default ray tracing configuration.
Returns
-------
dict
Default configuration for ray tracing.
"""
return {
"zenith_angle": {
"len": 1,
"unit": u.Unit("deg"),
"default": 20.0 * u.deg,
"names": ["zenith", "theta"],
},
"off_axis_angle": {
"len": 1 if config_runner else None,
"unit": u.Unit("deg"),
"default": 0.0 * u.deg,
"names": ["offaxis", "offset"],
},
"source_distance": {
"len": 1,
"unit": u.Unit("km"),
"default": 10.0 * u.km,
"names": ["sourcedist", "srcdist"],
},
"single_mirror_mode": {"len": 1, "default": False},
"use_random_focal_length": {"len": 1, "default": False},
"mirror_numbers": {
"len": 1 if config_runner else None,
"default": 1 if config_runner else "all",
},
}