from collections.abc import Callable
from datetime import timedelta, datetime
from altbacken.core.annealing import AnnealingState
[docs]
class TemperatureThreshold:
"""
Represents a temperature threshold for an annealing process.
This class is used to determine whether a given annealing state's
temperature has fallen below a specified threshold. It helps in controlling
the termination criteria for algorithms based on simulated annealing
techniques.
Attributes:
threshold (float): Specifies the non-negative temperature value used
as the limit for comparison.
"""
[docs]
def __init__(self, threshold: float):
if threshold < 0:
raise ValueError("Threshold must be non-negative")
self.threshold = threshold
[docs]
def __call__(self, state: AnnealingState) -> bool:
return state.temperature < self.threshold
def __str__(self) -> str:
return f"Temperature less than {self.threshold}"
[docs]
class IterationThreshold:
"""Represents a stopping criterion based on a threshold iteration count.
This class is used to determine whether a process, such as an optimization
algorithm, should stop based on the current iteration surpassing a predefined
iteration threshold. It can be utilized as a callable object in these scenarios.
Attributes:
threshold (int): The iteration count threshold. The process stops
when the current iteration reaches or exceeds this value.
"""
[docs]
def __init__(self, threshold: int):
if threshold <= 0:
raise ValueError("Threshold must be positive")
self.threshold = threshold
[docs]
def __call__(self, state: AnnealingState) -> bool:
return state.iteration >= self.threshold
def __str__(self) -> str:
return f"At most {self.threshold} iterations"
[docs]
class SolutionImprovementThreshold:
"""
Monitors the solution improvement status during an optimization process.
This class is designed to track whether a predefined threshold of iterations
without solution improvement is met. It can be used in annealing or other
iterative optimization algorithms to determine when to terminate the process
if sufficient improvement is not observed.
Attributes:
max_iterations_without_improvement (int): Maximum number of iterations
allowed without any improvement in the solution before termination.
"""
[docs]
def __init__(self, max_iterations_without_improvement: int):
if max_iterations_without_improvement <= 0:
raise ValueError("Maximum iterations without improvement must be positive")
self._max_iterations_without_improvement: int = max_iterations_without_improvement
self._last_improvement_iteration: int = 0
self._last_best_value: float = float("inf")
[docs]
def __call__(self, state: AnnealingState) -> bool:
"""
Evaluate whether the stop condition is met.
.. note::
This class is stateful. Its internal counter resets automatically
when ``state.iteration == 0``, which is the case at the start of
every :meth:`~altbacken.core.annealing.SimulatedAnnealing.simulate`
call. Reusing the same instance across multiple ``simulate()`` calls
is therefore safe. However, if you use this condition in a custom
loop where the iteration counter does not start at 0, the reset will
not trigger and the internal state will carry over from the previous
run.
"""
if state.iteration == 0:
self._last_best_value = float("inf")
self._last_improvement_iteration = 0
if state.best_value < self._last_best_value:
self._last_best_value = state.best_value
self._last_improvement_iteration = state.iteration
return state.iteration - self._last_improvement_iteration >= self._max_iterations_without_improvement
[docs]
class TimeThreshold:
"""
Represents a time-based threshold for tracking the progress of an annealing process.
This class is used to determine whether a given amount of time has passed since the
start of the annealing process.
"""
[docs]
def __init__(self, threshold: timedelta | float, timer: Callable[[], datetime] = datetime.now):
"""
Initializes the timer with a specified threshold and sets an end time based on this threshold.
Args:
threshold (timedelta | float): Time duration for the threshold. If a float is provided, it is interpreted
as seconds. It must be positive.
timer (Callable[[], datetime]): A function that returns the current time. Defaults to datetime.now.
Useful for deterministic testing.
Raises:
ValueError: If the given threshold is less than or equal to zero.
"""
self._threshold: timedelta = threshold if isinstance(threshold, timedelta) else timedelta(seconds=threshold)
if self._threshold.total_seconds() <= 0:
raise ValueError("Threshold must be positive")
self._timer = timer
self._end: datetime = self._timer() + self._threshold
@property
def threshold(self) -> timedelta:
"""
Provides access to the threshold value as a read-only property.
Returns:
timedelta: The threshold value.
"""
return self._threshold
[docs]
def __call__(self, state: AnnealingState) -> bool:
"""
Determines whether the annealing process should be terminated based on the
current iteration or elapsed time.
This callable method evaluates two conditions. It resets the timer if the
current iteration is the initial one (iteration 0) and ensures the optimization
process continues. For all subsequent iterations, it checks if the current
time has reached or surpassed the predetermined end time, determining if the
termination condition has been met.
Args:
state (AnnealingState): The current state of the annealing process,
including its iteration count and other relevant details.
Returns:
bool: True if the termination condition is satisfied, otherwise False.
.. note::
This class is stateful. The internal timer resets automatically
when ``state.iteration == 0``, which is the case at the start of
every :meth:`~altbacken.core.annealing.SimulatedAnnealing.simulate`
call. Reusing the same instance across multiple ``simulate()`` calls
is therefore safe. However, if you use this condition in a custom
loop where the iteration counter does not start at 0, the reset will
not trigger and the timer will continue from where it left off.
"""
if state.iteration == 0:
self._reset_timer()
return False
else:
return self._timer() >= self._end
def _reset_timer(self):
self._end = self._timer() + self._threshold