Source code for simtel.simtel_seeds

"""Seeds for sim_telarray simulations."""

import logging
from pathlib import Path

from simtools.constants import SIMTEL_MAX_SEED
from simtools.context import RunContext
from simtools.utils import names, random
from simtools.version import semver_to_int

SIMULATION_SEED_LENGTH_ERROR = (
    "simulation_seed must contain one value (simulation seed) or two values "
    "(instrument seed, simulation seed)."
)


def _split_simulation_seed(value):
    """Return (instrument seed or None, simulation seed) of a one- or two-value seed list."""
    if len(value) not in (1, 2):
        raise ValueError(SIMULATION_SEED_LENGTH_ERROR)
    instrument = value[0] if len(value) == 2 else None
    return instrument, value[-1]


[docs] class SimtelSeeds: """Manage seeds for sim_telarray simulations.""" def __init__( self, output_path=None, site=None, model_version=None, zenith_angle=None, azimuth_angle=None, context=None, ): """ Initialize seeds for sim_telarray simulations. Two seeds are set for sim_telarray simulations: - Instrument seed: used to randomize the instrument setup - Simulation seed: used for the shower simulation Parameters ---------- output_path : str or Path or None Output path for the seed file. site : str or None Site name. model_version : str or None Model version. zenith_angle : float or None Zenith angle. azimuth_angle : float or None Azimuth angle. """ self._logger = logging.getLogger(__name__) context = context or RunContext.from_environment() self.instrument_seed = context.args.get("instrument_seed", None) self.instruments = context.args.get("random_instrument_instances", None) self.simulation_seed = context.args.get("simulation_seed", None) self.seed_file = context.args.get("sim_telarray_seed_file", None) if output_path is not None: self.seed_file = Path(output_path) / self.seed_file self.seed_string = self.initialize_seeds(site, model_version, zenith_angle, azimuth_angle)
[docs] def initialize_seeds(self, site, model_version, zenith_angle, azimuth_angle): """Initialize seeds based on provided parameters.""" if isinstance(self.simulation_seed, list): fixed_instrument, simulation_seed = _split_simulation_seed(self.simulation_seed) if fixed_instrument is not None: return self._set_fixed_seeds() self.simulation_seed = simulation_seed if not self.simulation_seed: self.simulation_seed = random.seeds(max_seed=SIMTEL_MAX_SEED) if not self.instruments or self.instruments <= 1: return self._generate_seed_pair() return self._generate_seeds_with_file(site, model_version, zenith_angle, azimuth_angle)
def _set_fixed_seeds(self): """ Set fixed seeds to be using for testing purposes only. Fixes both instrument and simulation seeds. """ try: seed_string = f"{self.simulation_seed[0]},{self.simulation_seed[1]}" except IndexError as exc: raise IndexError( "Two seeds must be provided for testing purposes: " "first for instrument, second for shower simulation." ) from exc self._logger.warning(f"Using fixed test seeds: {seed_string}") return seed_string def _generate_seed_pair(self): """Generate seed string.""" if not self.instrument_seed: self.instrument_seed = random.seeds(max_seed=SIMTEL_MAX_SEED) self._logger.info( f"Generated sim_telarray seeds - Instrument: {self.instrument_seed}, " f"Shower simulation: {self.simulation_seed}" ) return f"{self.instrument_seed},{self.simulation_seed}" def _generate_seeds_with_file(self, site, model_version, zenith_angle, azimuth_angle): """Generate a seed file for the instrument seeds and return the seed string.""" self.instrument_seed = self._get_instrument_seed( site, model_version, zenith_angle, azimuth_angle ) self._logger.info( f"Writing random instrument seed file {self.seed_file}" f" (instrument seed {self.instrument_seed})" ) if self.instruments > 1024: raise ValueError("Number of random instances of instrument must be less than 1024") random_integers = random.instance_seeds( self.instrument_seed, self.instruments, max_seed=SIMTEL_MAX_SEED ) with open(self.seed_file, "w", encoding="utf-8") as file: file.write( "# Random seeds for instrument configuration generated with seed " f"{self.instrument_seed} (model version {model_version}, site {site})\n" f"# Zenith angle: {zenith_angle}, Azimuth angle: {azimuth_angle}\n" ) for number in random_integers: file.write(f"{number}\n") return f"file-by-run:{self.seed_file},{self.simulation_seed}" def _get_instrument_seed(self, site, model_version, zenith_angle, azimuth_angle): """ Get configuration dependent instrument seed. Three different scenarios are possible: - instrument seed provided through configuration: use it - site, model_version, zenith_angle, azimuth_angle provided: generate a seed based on these parameters - none of the above: generate a random seed Parameters ---------- site : str or None Site name. model_version : str or None Model version. zenith_angle : float or None Zenith angle. azimuth_angle : float or None Azimuth angle. Returns ------- int Instrument seed. """ # Use the instrument seed from the configuration if provided if self.instrument_seed: return self.instrument_seed # Generate a seed based on site, model_version, zenith_angle, and azimuth_angle if model_version and zenith_angle is not None and azimuth_angle is not None: try: key_index = next( i + 1 for i, (_, values) in enumerate(names.site_names().items()) if site.lower() in values ) except StopIteration as exc: raise ValueError(f"Unknown site: {site!r}") from exc seed = semver_to_int(model_version) * 10000000 seed = seed + key_index * 1000000 seed = seed + int(zenith_angle) * 1000 return seed + int(azimuth_angle) # Generate a random instrument seed return random.seeds(max_seed=SIMTEL_MAX_SEED)
[docs] def instance_seed_pairs(instances=1, context=None): """ Instrument and simulation seed of each random instrument instance. Uses the configured ``instrument_seed`` and ``simulation_seed`` (two values: instrument and simulation seed, for testing); seeds not configured are drawn at random. A single instance uses the seeds themselves, more instances derive theirs from them (as the instrument seed file does). Used by both ray-tracing backends. Parameters ---------- instances : int Number of random instrument instances. Returns ------- list of tuple of int (instrument seed, simulation seed) of each instance. """ context = context or RunContext.from_environment() instrument = context.args.get("instrument_seed") simulation = context.args.get("simulation_seed") if isinstance(simulation, list | tuple): fixed_instrument, simulation = _split_simulation_seed(simulation) if fixed_instrument is not None: instrument = fixed_instrument instrument = instrument or random.seeds(max_seed=SIMTEL_MAX_SEED) simulation = simulation or random.seeds(max_seed=SIMTEL_MAX_SEED) logging.getLogger(__name__).info( f"Instrument seed {instrument}, simulation seed {simulation} ({instances} instances)" ) return list( zip( random.instance_seeds(instrument, instances, max_seed=SIMTEL_MAX_SEED), random.instance_seeds(simulation, instances, max_seed=SIMTEL_MAX_SEED), strict=True, ) )