"""
A temperature schedule as needed by Simulated Annealing.
The Simulated Annealing algorithm implemented in
:mod:`~moptipy.algorithms.so.simulated_annealing` performs a local search that
always accepts a non-worsening move, i.e., a solution which is not worse than
the currently maintained one. However, it will also *sometimes* accept one
that is worse. The probability of doing so depends on how much worse that
solution is and on the current *temperature* of the algorithm. The higher the
temperature, the higher the acceptance probability. The temperature changes
over time according to the
:class:`~moptipy.algorithms.modules.temperature_schedule.TemperatureSchedule`.
The temperature schedule receives an iteration index `tau` as input and
returns the current temperature via :meth:`~moptipy.algorithms.modules.\
temperature_schedule.TemperatureSchedule.temperature`. Notice that `tau` is
zero-based for simplicity reason, meanings that the first objective function
evaluation is at index `0`.
"""
from math import e, isfinite, log
from typing import Final
from pycommons.types import type_error
from moptipy.api.component import Component
from moptipy.utils.logger import KeyValueLogSection
from moptipy.utils.strings import num_to_str_for_name
# start schedule
[docs]
class TemperatureSchedule(Component):
"""The base class for temperature schedules."""
def __init__(self, t0: float) -> None:
# end schedule
"""
Initialize the temperature schedule.
:param t0: the starting temperature, must be > 0
"""
super().__init__()
if not isinstance(t0, float):
raise type_error(t0, "t0", float)
if (not isfinite(t0)) or (t0 <= 0.0):
raise ValueError(f"t0 must be >0, cannot be {t0}.")
# start schedule
#: the starting temperature
self.t0: Final[float] = t0
[docs]
def temperature(self, tau: int) -> float:
"""
Compute the temperature at iteration `tau`.
:param tau: the iteration index, starting with `0` at the first
comparison of two solutions, at which point the starting
temperature :attr:`~TemperatureSchedule.t0` should be returned
:returns: the temperature
"""
# end schedule
[docs]
def log_parameters_to(self, logger: KeyValueLogSection) -> None:
"""
Log all parameters of this temperature schedule as key-value pairs.
:param logger: the logger for the parameters
>>> from moptipy.utils.logger import InMemoryLogger
>>> with InMemoryLogger() as l:
... with l.key_values("C") as kv:
... TemperatureSchedule(0.1).log_parameters_to(kv)
... text = l.get_log()
>>> text[1]
'name: TemperatureSchedule'
>>> text[3]
'T0: 0.1'
>>> len(text)
6
"""
super().log_parameters_to(logger)
logger.key_value("T0", self.t0)
# start exponential
[docs]
class ExponentialSchedule(TemperatureSchedule):
"""
The exponential temperature schedule.
The current temperature is computed as `t0 * (1 - epsilon) ** tau`.
>>> ex = ExponentialSchedule(10.0, 0.05)
>>> print(f"{ex.t0} - {ex.epsilon}")
10.0 - 0.05
>>> ex.temperature(0)
10.0
>>> ex.temperature(1)
9.5
>>> ex.temperature(2)
9.025
>>> ex.temperature(1_000_000_000_000_000_000)
0.0
"""
def __init__(self, t0: float, epsilon: float) -> None:
"""
Initialize the exponential temperature schedule.
:param t0: the starting temperature, must be > 0
:param epsilon: the epsilon parameter of the schedule, in (0, 1)
"""
super().__init__(t0)
# end exponential
if not isinstance(epsilon, float):
raise type_error(epsilon, "epsilon", float)
if (not isfinite(epsilon)) or (not (0.0 < epsilon < 1.0)):
raise ValueError(
f"epsilon cannot be {epsilon}, must be in (0,1).")
# start exponential
#: the epsilon parameter of the exponential schedule
self.epsilon: Final[float] = epsilon
#: the value used as basis for the exponent
self.__one_minus_epsilon: Final[float] = 1.0 - epsilon
# end exponential
if not (0.0 < self.__one_minus_epsilon < 1.0):
raise ValueError(
f"epsilon cannot be {epsilon}, because 1-epsilon must be in "
f"(0, 1) but is {self.__one_minus_epsilon}.")
# start exponential
[docs]
def temperature(self, tau: int) -> float:
"""
Compute the temperature at iteration `tau`.
:param tau: the iteration index, starting with `0` at the first
comparison of two solutions, at which point the starting
temperature :attr:`~TemperatureSchedule.t0` should be returned
:returns: the temperature
>>> s = ExponentialSchedule(100.0, 0.5)
>>> s.temperature(0)
100.0
>>> s.temperature(1)
50.0
>>> s.temperature(10)
0.09765625
"""
return self.t0 * (self.__one_minus_epsilon ** tau)
# end exponential
[docs]
def log_parameters_to(self, logger: KeyValueLogSection) -> None:
"""
Log all parameters of the exponential temperature schedule.
:param logger: the logger for the parameters
>>> from moptipy.utils.logger import InMemoryLogger
>>> with InMemoryLogger() as l:
... with l.key_values("C") as kv:
... ExponentialSchedule(0.2, 0.6).log_parameters_to(kv)
... text = l.get_log()
>>> text[1]
'name: exp0d2_0d6'
>>> text[3]
'T0: 0.2'
>>> text[5]
'e: 0.6'
>>> len(text)
8
"""
super().log_parameters_to(logger)
logger.key_value("e", self.epsilon)
def __str__(self) -> str:
"""
Get the string representation of the exponential temperature schedule.
:returns: the name of this schedule
>>> ExponentialSchedule(100.5, 0.3)
exp100d5_0d3
"""
return (f"exp{num_to_str_for_name(self.t0)}_"
f"{num_to_str_for_name(self.epsilon)}")
# start logarithmic
[docs]
class LogarithmicSchedule(TemperatureSchedule):
"""
The logarithmic temperature schedule.
The temperature is computed as `t0 / log(e + (tau * epsilon))`.
>>> lg = LogarithmicSchedule(10.0, 0.1)
>>> print(f"{lg.t0} - {lg.epsilon}")
10.0 - 0.1
>>> lg.temperature(0)
10.0
>>> lg.temperature(1)
9.651322627630812
>>> lg.temperature(1_000_000_000_000_000_000_000_000_000_000_000_000_000)
0.11428802155348732
"""
def __init__(self, t0: float, epsilon: float) -> None:
"""
Initialize the logarithmic temperature schedule.
:param t0: the starting temperature, must be > 0
:param epsilon: the epsilon parameter of the schedule, is > 0
"""
super().__init__(t0)
# end logarithmic
if not isinstance(epsilon, float):
raise type_error(epsilon, "epsilon", float)
if (not isfinite(epsilon)) or (epsilon <= 0.0):
raise ValueError(
f"epsilon cannot be {epsilon}, must be > 0.")
# start logarithmic
#: the epsilon parameter of the logarithmic schedule
self.epsilon: Final[float] = epsilon
[docs]
def temperature(self, tau: int) -> float:
"""
Compute the temperature at iteration `tau`.
:param tau: the iteration index, starting with `0` at the first
comparison of two solutions, at which point the starting
temperature :attr:`~TemperatureSchedule.t0` should be returned
:returns: the temperature
>>> s = LogarithmicSchedule(100.0, 0.5)
>>> s.temperature(0)
100.0
>>> s.temperature(1)
85.55435113150568
>>> s.temperature(10)
48.93345190925178
"""
return self.t0 / log(e + (tau * self.epsilon))
# end logarithmic
[docs]
def log_parameters_to(self, logger: KeyValueLogSection) -> None:
"""
Log all parameters of the logarithmic temperature schedule.
:param logger: the logger for the parameters
>>> from moptipy.utils.logger import InMemoryLogger
>>> with InMemoryLogger() as l:
... with l.key_values("C") as kv:
... LogarithmicSchedule(0.2, 0.6).log_parameters_to(kv)
... text = l.get_log()
>>> text[1]
'name: ln0d2_0d6'
>>> text[3]
'T0: 0.2'
>>> text[5]
'e: 0.6'
>>> len(text)
8
"""
super().log_parameters_to(logger)
logger.key_value("e", self.epsilon)
def __str__(self) -> str:
"""
Get the string representation of the logarithmic temperature schedule.
:returns: the name of this schedule
>>> LogarithmicSchedule(100.5, 0.3)
ln100d5_0d3
"""
return (f"ln{num_to_str_for_name(self.t0)}_"
f"{num_to_str_for_name(self.epsilon)}")