# Copyright (c) 2023 Cisco and/or its affiliates. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Module defining Config class.""" from collections.abc import Iterable from dataclasses import dataclass from typing import Optional from .dataclass import DataclassProperty from .search_goal import SearchGoal from .search_goal_tuple import SearchGoalTuple @dataclass class Config: """Structure containing several static config items. The main MLRsearch algorithm uses multiple customizable values. Pylint complains if the values appear as long argument lists or multiple local variables. This class offers a storage for values which do not contain internally mutable state and are set at an unknown time before the search starts. This way users can override only some values, and do it over multiple calls. All "official" user inputs are contained here. Properties are defined to enforce the requirements on allowed values. All fields have default values, so instances can be created without any. It is still recommended to set all values after instantiation, as the defaults may change in the next version. As some relations between values of different fields are required, users must take care to set them in the correct order. For example, min_load has to be set to a value smaller than the current value of max_load. """ # Externally visible "fields" (but in fact redefined as properties). goals: SearchGoalTuple = SearchGoalTuple((SearchGoal(),)) """Container holding search goals.""" min_load: float = 9001.0 """Each trial measurement must have intended load at least this [tps].""" max_load: float = 1e9 """Each trial measurement must have intended load at most this [tps].""" search_duration_max: float = 1200.0 """The search will end as a failure this long [s] after it is started.""" warmup_duration: float = 1.0 """If specified, one trial at max load and this duration is performed before the usual search starts. None converts to zero and means no warmup. The results of that one trial are ignored.""" @DataclassProperty def goals(self) -> SearchGoalTuple: """Return the reference to the current container of goals. :returns: The current container instance. :rtype: SearchGoalTuple """ return self._goals @goals.setter def goals(self, goals: Iterable[SearchGoal]) -> None: """Create and store the goal container. :param goals: Search goals to add to the container to store. :type goals: Iterable[SearchGoal] :raises ValueError: If there are no goals. :raises TypeError: If any of the goals is not a SearchGoal. """ self._goals = SearchGoalTuple(goals) @DataclassProperty def min_load(self) -> float: """Getter for min load, no logic here. :returns: Currently set minimal intended load [tps]. :rtype: float """ return self._min_load @min_load.setter def min_load(self, load: float) -> None: """Set min load after converting type and checking value. :param load: Minimal intended load [tps] to set. :type load: float :raises ValueError: If the argument is found invalid. """ load = float(load) if load <= 0.0: raise ValueError(f"Min load {load} must be positive.") # At the time init is first called, _max_load is not set yet. if hasattr(self, "_max_load") and load >= self.max_load: raise ValueError(f"Min load {load} must be smaller.") self._min_load = load @DataclassProperty def max_load(self) -> float: """Getter for max load, no logic here. :returns: Currently set maximal intended load [tps]. :rtype: float """ return self._max_load @max_load.setter def max_load(self, load: float) -> None: """Set max load after converting type and checking value. :param load: Minimal intended load [tps] to set. :type load: float :raises ValueError: If the argument is found invalid. """ load = float(load) if load <= self.min_load: raise ValueError(f"Max load {load} must be bigger.") self._max_load = load @DataclassProperty def search_duration_max(self) -> float: """Getter for max search duration, no logic here. :returns: Currently set max search duration [s]. :rtype: float """ return self._search_duration_max @search_duration_max.setter def search_duration_max(self, duration: float) -> None: """Set max search duration after converting and checking value. :param duration: Search duration maximum [s] to set. :type duration: float :raises ValueError: If the argument is found invalid. """ duration = float(duration) if duration <= 0.0: raise ValueError(f"Search duration max too small: {duration}") self._search_duration_max = duration @DataclassProperty def warmup_duration(self) -> float: """Getter for warmup duration, no logic here. :returns: Currently set max search duration [s]. :rtype: float """ return self._warmup_duration @warmup_duration.setter def warmup_duration(self, duration: Optional[float]) -> None: """Set warmup duration after converting and checking value. Zero duration is treated as None, meaning no warmup trial. :param duration: Warmup duration [s] to set. :type duration: Optional(float) :raises ValueError: If the argument is found invalid. """ if duration: duration = float(duration) if duration < 0.0: raise ValueError(f"Warmup duration too small: {duration}") else: duration = 0.0 self._warmup_duration = duration