#!/usr/bin/python3
"""Configuration file writer for sim_telarray."""
import logging
from copy import deepcopy
from pathlib import Path
import astropy.units as u
import numpy as np
import simtools.version
from simtools import dependencies
from simtools.constants import SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH
from simtools.context import RunContext
from simtools.data_model import parameter_catalog as catalog
from simtools.data_model import schema
from simtools.data_model.table_asset import get_simtel_serialization
from simtools.model.mirrors import uses_segmented_dual_mirror_geometry
from simtools.model_repository.asset_names import get_simtel_table_file_name
from simtools.simtel import (
segmentation,
sim_telarray_metadata,
simtel_file_writer,
table_serializers,
)
from simtools.utils import names, value_conversion
logger = logging.getLogger(__name__)
_CAMERA_COMPONENT_PARAMETERS = (
"camera_rotate",
"camera_pixel_types",
"camera_pixel_layout",
"camera_trigger_groups",
"camera_trigger_members",
)
_FLASHER_LEGACY_PARAMETERS = ("flasher_pulse_width", "flasher_pulse_exp_decay")
_LIGHTGUIDE_KEYS = (
("lightguide_angle_parameter", "lightguide_angle_file"),
("lightguide_wavelength_parameter", "lightguide_wavelength_file"),
)
_SKIPPED_PARAMETERS = (
*_CAMERA_COMPONENT_PARAMETERS,
"camera_config_file",
*_FLASHER_LEGACY_PARAMETERS,
)
[docs]
class SimtelConfigWriter:
"""
SimtelConfigWriter writes sim_telarray configuration files.
It is designed to be used by model classes (TelescopeModel and ArrayModel) only.
Parameters
----------
site: str
South or North.
model_version: str
Model version.
telescope_model_name: str
Telescope model name.
layout_name: str
Layout name.
label: str
Instance label. Important for output file naming.
"""
TAB = " " * 3
def __init__(
self,
site,
model_version,
layout_name=None,
telescope_model_name=None,
telescope_design_model=None,
label=None,
model_reader=None,
context=None,
):
"""Initialize SimtelConfigWriter."""
self._site = site
self._model_version = model_version
self._label = label
self._layout_name = layout_name
self._telescope_model_name = telescope_model_name
self._telescope_design_model = telescope_design_model
self._model_reader = model_reader
self._context = context or RunContext.from_environment()
self._serialized_table_cache = {}
self._camera_file_cache = {}
@property
def label(self):
"""Return the label used for generated configuration files."""
return self._label
[docs]
def write_telescope_config_file(
self, config_file_path, parameters, telescope_name=None, telescope_design_model=None
):
"""
Write the sim_telarray config file for a single telescope.
Parameters
----------
config_file_path: str or Path
Path of the file to write on.
parameters: dict
Model parameters
telescope_name: str
Name of the telescope (use self._telescope_model_name if None)
telescope_design_model: str
Telescope design model.
"""
logger.debug(f"Writing telescope config file {config_file_path}")
telescope_name = telescope_name or self._telescope_model_name
_telescope_design_model = telescope_design_model or self._telescope_design_model
simtel_par = self._get_parameters_for_sim_telarray(
parameters, config_file_path, telescope_name=telescope_name
)
with open(config_file_path, "w", encoding="utf-8") as file:
self._write_header(file, "TELESCOPE CONFIGURATION FILE")
file.write(
"#ifdef TELESCOPE\n"
f" echo Configuration for {telescope_name} - TELESCOPE $(TELESCOPE)\n"
"#endif\n\n"
)
for simtel_name, simtel_value in simtel_par.items():
file.write(f"{simtel_name} = {self._get_value_string_for_simtel(simtel_value)}\n")
metadata = self._get_sim_telarray_metadata(
"telescope",
parameters,
telescope_name,
telescope_design_model=_telescope_design_model,
additional_meta_parameters=(
["camera_config_file"] if "camera_config_file" in simtel_par else []
),
)
file.write("".join(f"{meta}\n" for meta in metadata))
def _get_parameters_for_sim_telarray(self, parameters, config_file_path, telescope_name=None):
"""Convert model parameters to sim_telarray names and values."""
simtel_par = {}
camera_file = self._write_camera_file(
parameters, config_file_path, telescope_name=telescope_name
)
if camera_file is not None:
simtel_par["camera_config_file"] = camera_file
elif "camera_config_file" in parameters:
simtel_par["camera_config_file"] = parameters["camera_config_file"]["value"]
for par, value in parameters.items():
if par in _SKIPPED_PARAMETERS:
continue
if par == "mirror_list" and uses_segmented_dual_mirror_geometry(parameters):
simtel_par["mirror_list"] = None
continue
simtel_name, simtel_value = self._convert_model_parameters_to_simtel_format(
self._get_sim_telarray_config_parameter_name(par),
value["value"],
config_file_path,
None,
parameter_name=par,
parameter_data=value,
)
if simtel_name:
simtel_par[simtel_name] = simtel_value
if "stars" not in parameters: # sim_telarray requires 'stars' to be set
simtel_par["stars"] = None
return dict(
sorted(self._get_flasher_parameters_for_sim_telarray(parameters, simtel_par).items())
)
def _write_camera_file(self, parameters, config_file_path, telescope_name=None):
"""Write a sim_telarray camera file from independent camera parameters."""
missing = sorted(set(_CAMERA_COMPONENT_PARAMETERS) - set(parameters))
if len(missing) == len(_CAMERA_COMPONENT_PARAMETERS):
return None
if missing:
raise ValueError(f"Camera component parameters are missing: {missing}")
destination = Path(config_file_path).parent
telescope_name = telescope_name or Path(config_file_path).stem
cache_key = repr(
tuple((name, parameters.get(name)) for name in _CAMERA_COMPONENT_PARAMETERS)
)
shared_camera = self._telescope_design_model is not None and all(
parameters[name].get("instrument") == self._telescope_design_model
for name in _CAMERA_COMPONENT_PARAMETERS
)
camera_name = self._telescope_design_model if shared_camera else telescope_name
output = destination / f"camera-{camera_name}.dat"
if output.is_file() and (shared_camera or self._camera_file_cache.get(output) == cache_key):
return output.name
pixel_types = self._write_lightguide_tables(parameters, destination, telescope_name)
pixels = self._validated_camera_pixels(parameters)
configuration = {
"rotate": self._parameter_value(parameters, "camera_rotate"),
"pixel_types": pixel_types,
"pixels": pixels,
"triggers": self._camera_table_records(parameters, "camera_trigger_groups"),
"trigger_members": self._camera_table_records(parameters, "camera_trigger_members"),
}
result = simtel_file_writer.write_camera_file(configuration, output)
self._camera_file_cache[output] = cache_key
return result
def _write_lightguide_tables(self, parameters, destination, telescope_name):
"""Return pixel types with light-guide table references written to sim_telarray files."""
pixel_types = deepcopy(self._parameter_value(parameters, "camera_pixel_types"))
for pixel_type in pixel_types:
for parameter_key, output_key in _LIGHTGUIDE_KEYS:
parameter_name = pixel_type.pop(parameter_key, None)
if parameter_name is not None:
pixel_type[output_key] = self._write_parameter_table(
parameter_name,
parameters[parameter_name],
destination,
f"{parameter_name}-{telescope_name}.dat",
require_rows=True,
)
return pixel_types
def _validated_camera_pixels(self, parameters):
"""Return camera pixel rows after checking them against the expected pixel count."""
pixels = self._camera_table_records(parameters, "camera_pixel_layout")
expected_pixels = self._parameter_value(parameters, "camera_pixels")
if expected_pixels is not None and int(expected_pixels) != len(pixels):
raise ValueError(
f"camera_pixels={expected_pixels} does not match camera layout rows={len(pixels)}"
)
return pixels
@staticmethod
def _parameter_value(parameters, parameter_name):
"""Return a selected parameter value or ``None`` for an absent optional value."""
return (parameters.get(parameter_name) or {}).get("value")
def _camera_table_records(self, parameters, parameter_name):
"""Return the rows of an exported camera component table as scalar dicts."""
parameter_data = parameters.get(parameter_name)
if parameter_data is None:
raise ValueError(f"Camera component parameter is missing: {parameter_name}")
table = self._read_parameter_table(parameter_data)
columns = [[getattr(v, "value", v) for v in table[column]] for column in table.colnames]
return [dict(zip(table.colnames, row)) for row in zip(*columns)]
def _read_parameter_table(self, parameter_data):
"""Return the astropy table of a model parameter via the model reader."""
if self._model_reader is None:
raise RuntimeError("sim_telarray table serialization requires a model reader")
return self._model_reader.get_parameter_table(parameter_data)
def _write_parameter_table(
self, parameter_name, parameter_data, destination, default_name, require_rows=False
):
"""Write an ECSV model parameter as sim_telarray table unless it is already up to date."""
contract = get_simtel_serialization(
schema.get_model_parameter_schema(
parameter_name, parameter_data.get("model_parameter_schema_version")
)
)
shared_name = get_simtel_table_file_name(parameter_data)
output_name = shared_name or default_name
output_path = Path(destination) / output_name
serialization_key = repr(((parameter_name, parameter_data), contract))
if not (
output_path.is_file()
and (
shared_name is not None
or self._serialized_table_cache.get(output_path) == serialization_key
)
):
table = self._read_parameter_table(parameter_data)
if require_rows and len(table) == 0:
raise ValueError(f"Selected lightguide parameter is empty: {parameter_name}")
table_serializers.write_simtel_table(
table, destination, contract=contract, output_name=output_name
)
self._serialized_table_cache[output_path] = serialization_key
return output_name
@staticmethod
def _get_sim_telarray_config_parameter_name(parameter_name):
"""Return the sim_telarray name when it is an ordinary config parameter."""
simtel_name = catalog.get_simulation_software_name_from_parameter_name(
parameter_name, software_name="sim_telarray"
)
mode = catalog.get_simulation_software_meta_parameter_mode(
parameter_name, software_name="sim_telarray"
)
return None if simtel_name and mode == "set" else simtel_name
def _get_flasher_parameters_for_sim_telarray(self, parameters, simtel_par):
"""Combine flasher pulse time parameters into the single sim_telarray parameter set."""
if "flasher_pulse_shape" not in parameters:
return simtel_par
mapping = {
"gauss": "laser_pulse_sigtime",
"tophat": "laser_pulse_twidth",
"gauss-exponential": "laser_pulse_sigtime",
}
shape_value = self._parameter_value(parameters, "flasher_pulse_shape")
if isinstance(shape_value, str):
shape = shape_value.lower()
width = self._parameter_value(parameters, "flasher_pulse_width")
width = 0.0 if width is None else width
exp_decay = self._parameter_value(parameters, "flasher_pulse_exp_decay")
exp_decay = 0.0 if exp_decay is None else exp_decay
else:
shape, width, exp_decay = shape_value
shape = shape.lower()
simtel_par.pop("flasher_pulse_width", None)
simtel_par.pop("flasher_pulse_exp_decay", None)
simtel_par["laser_pulse_exptime"] = exp_decay if ("exponential" in shape) else 0.0
simtel_par.update(dict.fromkeys(mapping.values(), 0.0))
if shape in mapping:
simtel_par[mapping[shape]] = width
elif shape != "exponential":
logger.warning(f"Flasher pulse shape '{shape}' without width definition")
return simtel_par
def _get_value_string_for_simtel(self, value):
"""Return a value string for simtel (none, 0/1 for booleans, space-separated lists)."""
value = "none" if value is None else value # simtel requires 'none'
if isinstance(value, bool):
value = 1 if value else 0
elif isinstance(value, list | np.ndarray):
value = value_conversion.convert_list_to_string(value, shorten_list=True)
return value
def _config_type_meta_parameters(self, config_type, telescope_model_name, design_model):
"""Return the release/config header lines and metaparam prefix for a config type."""
meta_parameters = [
f"config_release = {self._model_version} with simtools v{simtools.version.__version__}",
f"config_version = {self._model_version}",
]
design_model = design_model or "design_model_not_set"
if config_type == "telescope":
meta_parameters.extend(
[
f"camera_config_name = {design_model}",
f"camera_config_variant = {telescope_model_name}",
f"camera_config_version = {self._model_version}",
f"optics_config_name = {design_model}",
f"optics_config_variant = {telescope_model_name}",
f"optics_config_version = {self._model_version}",
]
)
return meta_parameters, "metaparam telescope"
if config_type == "site":
meta_parameters.extend(
[
f"site_config_name = {self._site}",
"site_config_variant = ",
f"site_config_version = {self._model_version}",
f"array_config_name = {self._layout_name}",
"array_config_variant = ",
f"array_config_version = {self._model_version}",
"metaparam global add random_seed",
]
)
return meta_parameters, "metaparam global"
raise ValueError(f"Unknown metadata type {config_type}")
@staticmethod
def _model_parameter_meta_lines(model_parameters, prefix):
"""Return add/set metaparam lines for model parameters known to sim_telarray."""
lines = []
for key, value in (model_parameters or {}).items():
if key == "camera_config_file":
continue
simtel_name = catalog.get_simulation_software_name_from_parameter_name(
key, software_name="sim_telarray"
)
mode = catalog.get_simulation_software_meta_parameter_mode(
key, software_name="sim_telarray"
)
if simtel_name and mode == "add":
lines.append(f"{prefix} add {simtel_name}")
elif simtel_name and mode == "set":
lines.append(f"{prefix} set {simtel_name}={value['value']}")
return lines
def _get_sim_telarray_metadata(
self,
config_type,
model_parameters,
telescope_model_name,
additional_metadata=None,
telescope_design_model=None,
additional_meta_parameters=None,
):
"""Return sim_telarray metadata lines for a 'telescope' or 'site' configuration file."""
meta_parameters, prefix = self._config_type_meta_parameters(
config_type, telescope_model_name, telescope_design_model
)
meta_parameters.extend(self._model_parameter_meta_lines(model_parameters, prefix))
for parameter in additional_meta_parameters or []:
meta_parameters.append(f"{prefix} add {parameter}")
for key, value in (additional_metadata or {}).items():
if value is not None:
meta_parameters.append(f"{prefix} set {key}={value}")
sim_telarray_metadata.validate_metadata(meta_parameters)
return meta_parameters
[docs]
def write_array_config_file(
self, config_file_path, telescope_model, site_model, additional_metadata=None
):
"""
Write the sim_telarray config file for an array of telescopes.
Parameters
----------
config_file_path: str or Path
Path of the file to write on.
telescope_model: dict of TelescopeModel
Dictionary of TelescopeModel's instances as used by the ArrayModel instance.
site_model: Site model
Site model.
additional_metadata: dict
Dictionary with additional metadata to include.
Raises
------
FileExistsError
If the config file already exists.
"""
config_file_directory = Path(config_file_path).parent
config_file_path = Path(config_file_path)
if config_file_path.exists():
raise FileExistsError(f"Array config file {config_file_path} already exists.")
with open(config_file_path, "w", encoding="utf-8") as file:
self._write_header(file, "ARRAY CONFIGURATION FILE")
file.write("#ifndef TELESCOPE\n# define TELESCOPE 0\n#endif\n\n")
# TELESCOPE 0 - global parameters
file.write(
"#if TELESCOPE == 0\n"
f"{self.TAB}echo *****************************\n"
f"{self.TAB}echo Site: {self._site}\n"
f"{self.TAB}echo LayoutName: {self._layout_name}\n"
f"{self.TAB}echo ModelVersion: {self._model_version}\n"
f"{self.TAB}echo *****************************\n\n"
)
self._write_simtools_parameters(file)
global_parameters = {
name: parameter
for name, parameter in {
**site_model.get_simulation_software_parameters("sim_telarray"),
**site_model.parameters,
}.items()
if not catalog.is_global_sim_telarray_parameter(name)
}
self._write_site_parameters(
file,
global_parameters,
config_file_path,
telescope_model,
additional_metadata,
)
file.write(self.TAB + f"maximum_telescopes = {len(telescope_model)}\n\n")
# Default telescope in sim_telarray - 0th tel in telescope list
_, first_telescope = next(iter(telescope_model.items()))
invalid_telescope_name = "InvalidTelescope"
invalid_telescope_config = f"{invalid_telescope_name}.cfg"
self._validate_include_file_name_length(invalid_telescope_config)
file.write(f"# include <{invalid_telescope_config}>\n\n")
self.write_dummy_telescope_configuration_file(
deepcopy(first_telescope.parameters),
config_file_directory / f"{invalid_telescope_name}.cfg",
invalid_telescope_name,
)
for count, (tel_name, tel_model) in enumerate(telescope_model.items()):
tel_config_file = tel_model.config_file_path.name
self._validate_include_file_name_length(tel_config_file)
file.write(f"% {tel_name}\n")
file.write(f"#elif TELESCOPE == {count + 1}\n\n")
file.write(f"# include <{tel_config_file}>\n\n")
file.write("#endif \n\n") # configuration files need to end with \n\n
def _validate_include_file_name_length(self, file_name):
"""Validate include-file names against the sim_telarray parser word-length limit."""
if len(file_name) > SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH:
raise ValueError(
"sim_telarray include filename exceeds parser limit "
f"({len(file_name)}>{SIM_TELARRAY_INCLUDE_FILENAME_MAX_LENGTH}): {file_name}"
)
[docs]
def write_single_mirror_list_file(
self, mirror_number, mirrors, single_mirror_list_file, set_focal_length_to_zero=False
):
"""
Write the sim_telarray mirror list file for a single mirror.
Parameters
----------
mirror_number: int
Mirror number.
mirrors: Mirrors
Instance of Mirrors.
single_mirror_list_file: str or Path
Path of the file to write on.
set_focal_length_to_zero: bool
Flag to set the focal length to zero.
"""
_, _, mirror_panel_diameter, focal_length, shape_type = (
mirrors.get_single_mirror_parameters(mirror_number)
)
with open(single_mirror_list_file, "w", encoding="utf-8") as file:
self._write_header(file, "MIRROR LIST FILE", "#")
file.write(
"# Column 1: X pos. [cm] (North/Down)\n"
"# Column 2: Y pos. [cm] (West/Right from camera)\n"
"# Column 3: flat-to-flat diameter [cm]\n"
"# Column 4: focal length [cm], typically zero = adapting in sim_telarray.\n"
"# Column 5: shape type: 0=circular, 1=hex. with flat side parallel to y, "
"2=square, 3=other hex. (default: 0)\n"
"# Column 6: Z pos (height above dish backplane) [cm], typ. omitted (or zero)"
" to adapt to dish shape settings.\n"
"#\n"
f"0. 0. {mirror_panel_diameter.to('cm').value} "
f"{focal_length.to('cm').value if not set_focal_length_to_zero else 0} "
f"{shape_type} 0.\n"
)
def _write_header(self, file, title, comment_char="%"):
"""Write a generic header with site, model version and optional names."""
sep = f"{comment_char}{50 * '='}"
optional = {
"TelescopeModelName": self._telescope_model_name,
"LayoutName": self._layout_name,
"Label": self._label,
}
lines = [
sep,
f"{comment_char} {title}",
f"{comment_char} Site: {self._site}",
f"{comment_char} ModelVersion: {self._model_version}",
*(f"{comment_char} {key}: {val}" for key, val in optional.items() if val is not None),
sep,
comment_char,
]
file.write("".join(f"{line}\n" for line in lines))
def _write_simtools_parameters(self, file):
"""Write simtools-specific parameters as metadata."""
meta_items = {
"simtools_version": simtools.version.__version__,
"simtools_model_production_version": self._model_version,
**dependencies.get_dependency_metadata(context=self._context),
}
try:
build_opts = dependencies.get_build_options(context=self._context)
for key, value in build_opts.items():
if key in dependencies.SIMTEL_METADATA_BUILD_OPTION_KEYS:
meta_items[f"simtools_{key}"] = value
except FileNotFoundError, TypeError:
pass # don't expect build_opts.yml to be present on all systems
# CORSIKA executable without _flat/_curved suffix (do not know here if curved or flat)
try:
meta_items["simtools_corsika_exec"] = (
self._context.software.corsika_exe.name.removesuffix("_flat")
)
except AttributeError as exc:
raise AttributeError("CORSIKA executable path is not set in settings.") from exc
file.write(f"{self.TAB}% Simtools parameters\n")
meta_lines = [
f"metaparam global set {key} = {value}"
for key, value in meta_items.items()
if not isinstance(value, list)
]
sim_telarray_metadata.validate_metadata(meta_lines)
file.write("".join(f"{self.TAB}{line}\n" for line in meta_lines))
def _write_site_parameters(
self, file, site_parameters, model_path, telescope_model, additional_metadata=None
):
"""Write site parameters and their sim_telarray metadata."""
file.write(self.TAB + "% Site parameters\n")
for par, value in site_parameters.items():
simtel_name, simtel_value = self._convert_model_parameters_to_simtel_format(
self._get_sim_telarray_config_parameter_name(par),
value["value"],
model_path,
telescope_model,
parameter_name=par,
parameter_data=value,
)
if simtel_name is not None:
file.write(f"{self.TAB}{simtel_name} = {simtel_value}\n")
for meta in self._get_sim_telarray_metadata(
"site", site_parameters, None, additional_metadata
):
file.write(f"{self.TAB}{meta}\n")
file.write("\n")
def _convert_model_parameters_to_simtel_format(
self,
simtel_name,
value,
model_path,
telescope_model,
parameter_name=None,
parameter_data=None,
):
"""Convert a model parameter value to sim_telarray format (may write a table file)."""
if simtel_name is None:
return None, None
if simtel_name in {"primary_segmentation", "secondary_segmentation"} and isinstance(
value, list
):
instrument = parameter_data.get("instrument")
shared_output = instrument is not None and instrument == self._telescope_design_model
suffix = instrument if shared_output else Path(model_path).stem
output = Path(model_path).parent / f"{parameter_name}-{suffix}.dat"
if shared_output and output.is_file():
return simtel_name, output.name
return simtel_name, segmentation.write_mirror_segmentation(
value,
output,
parameter_name=parameter_name,
schema_version=parameter_data["model_parameter_schema_version"],
)
if isinstance(value, str) and value.lower().endswith(".ecsv"):
return simtel_name, self._write_table_parameter_file(
simtel_name, value, model_path, telescope_model, parameter_name, parameter_data
)
conversions = {
"array_triggers": lambda: self._write_array_triggers_file(
value, model_path, telescope_model
),
"fadc_pulse_shape": lambda: self._write_table_parameter_file(
"fadc_pulse_shape",
value,
model_path,
telescope_model,
parameter_name,
parameter_data,
),
}
try:
value = conversions[simtel_name]()
except KeyError:
pass
except AttributeError: # covers cases where telescope_model is None
return None, None
return simtel_name, value
def _write_table_parameter_file(
self,
parameter_name,
value,
model_path,
_telescope_model,
source_parameter=None,
parameter_data=None,
):
"""Write an ECSV table parameter as sim_telarray table; return its basename or value."""
if isinstance(value, str) and value.lower().endswith(".ecsv"):
if self._model_reader is None:
raise RuntimeError("sim_telarray table serialization requires a model reader")
name = source_parameter or parameter_name
return self._write_parameter_table(
name,
parameter_data,
Path(model_path).parent,
f"{name}-{Path(model_path).stem}.dat",
)
if not isinstance(value, dict):
return value
raise TypeError("sim_telarray table writing requires a validated Astropy ECSV table")
def _write_array_triggers_file(self, array_triggers, model_path, telescope_model):
"""Write the array trigger definition file (simtel format) and return its basename."""
trigger_per_telescope_type = {}
for count, tel_name in enumerate(telescope_model.keys()):
telescope_type = names.get_array_element_type_from_name(tel_name)
trigger_per_telescope_type.setdefault(telescope_type, []).append(count + 1)
lines = []
non_hardstereo_groups = {}
all_non_hardstereo_tels = []
multiplicity = None
for tel_type, tel_list in trigger_per_telescope_type.items():
suffix = "_single_telescope" if len(tel_list) == 1 else "_array"
trigger = next((t for t in array_triggers if t["name"] == tel_type + suffix), None)
width = trigger["width"]["value"] * u.Unit(trigger["width"]["unit"]).to("ns")
sep = trigger["min_separation"]
minsep = None
if all(sep[key] is not None for key in ["value", "unit"]):
minsep = sep["value"] * u.Unit(sep["unit"]).to("m")
multiplicity = trigger["multiplicity"]["value"] # Store for later use
if trigger.get("hard_stereo", {}).get("value"):
lines.append(self._trigger_line(multiplicity, tel_list, width, minsep, True))
else:
non_hardstereo_groups.setdefault((width, minsep), []).extend(tel_list)
all_non_hardstereo_tels.extend(tel_list)
# Individual non-hardstereo groups if they have different parameters
if len(non_hardstereo_groups) > 1:
lines += [
self._trigger_line(multiplicity, tel_list, width, minsep)
for (width, minsep), tel_list in non_hardstereo_groups.items()
]
# Combined line with all non-hardstereo telescopes using shortest values
if all_non_hardstereo_tels:
group_keys = list(non_hardstereo_groups)
minseps = [minsep for _, minsep in group_keys if minsep is not None]
lines.append(
self._trigger_line(
multiplicity,
sorted(all_non_hardstereo_tels),
min(width for width, _ in group_keys),
min(minseps) if minseps else None,
)
)
array_triggers_file = "array_triggers.dat"
model_path = Path(model_path)
model_directory = model_path if model_path.is_dir() else model_path.parent
with open(model_directory / array_triggers_file, "w", encoding="utf-8") as file:
file.write("# Array trigger definition\n" + "".join(f"{line}\n" for line in lines))
return array_triggers_file
@staticmethod
def _trigger_line(multiplicity, tel_list, width, minsep, hardstereo=False):
"""Return one sim_telarray 'Trigger' line."""
line = f"Trigger {multiplicity} of " + ", ".join(map(str, tel_list)) + f" width {width}"
if hardstereo:
line += " hardstereo"
if minsep is not None:
line += f" minsep {minsep}"
return line
[docs]
def write_dummy_telescope_configuration_file(
self, parameters, config_file_path, telescope_name
):
"""
Write 'dummy' telescope configuration file used as zeroth telescope in sim_telarray.
Replaces key telescope configuration values with dummy values.
Parameters
----------
parameters: dict
Telescope parameters used as template.
config_file_path: str or Path
Path of the dummy configuration file to write on.
telescope_name: str
Name of the telescope.
"""
dummy_defaults = {
"camera_config_file": f"{telescope_name}_single_pixel_camera.dat",
"discriminator_pulse_shape": f"{telescope_name}_pulse.dat",
"fadc_pulse_shape": f"{telescope_name}_pulse.dat",
"mirror_list": f"{telescope_name}_single_12m_mirror.dat",
"mirror_reflectivity": f"{telescope_name}_reflectivity.dat",
"camera_pixels": 1,
"trigger_pixels": 1,
"num_gains": 1,
"fadc_bins": 10,
"disc_bins": 10,
"fadc_sum_bins": 10,
"fadc_sum_offset": 0,
"asum_threshold": 9999,
"dsum_threshold": 9999,
"discriminator_threshold": 9999,
"fadc_amplitude": 1.0,
"discriminator_amplitude": 1.0,
}
for key, val in dummy_defaults.items():
if key in parameters:
parameters[key]["value"] = val
parameters["camera_config_file"] = {"value": dummy_defaults["camera_config_file"]}
camera_components = {
n: parameters.pop(n) for n in _CAMERA_COMPONENT_PARAMETERS if n in parameters
}
try:
self.write_telescope_config_file(config_file_path, parameters, telescope_name)
finally:
parameters.update(camera_components)
t = telescope_name
dummy_files = {
f"{t}_single_12m_mirror.dat": "0 0 1200 0.0 0\n",
f"{t}_reflectivity.dat": "".join(f"{w} 0.8\n" for w in range(200, 801, 50)),
f"{t}_single_pixel_camera.dat": (
f'PixType 1 0 0 300 1 300 0.00 "{t}_funnels.dat"\n'
"Pixel 0 1 0. 0. 0 0 0 0x00 1\n"
"Trigger 1 of 0\n"
),
f"{t}_funnel.dat": "".join(f"{a} 0.78 1.5\n" for a in range(36)),
f"{t}_pulse.dat": "0 0 0\n1 1 1\n2 2 2\n3 3 3\n4 0 0\n",
}
config_directory = Path(config_file_path).parent
for file_name, text in dummy_files.items():
(config_directory / file_name).write_text(text, encoding="utf-8")