Source code for pybop.optimisers._simulated_annealing
import numpy as np
from pints import Optimiser as PintsOptimiser
[docs]
class SimulatedAnnealingImpl(PintsOptimiser):
"""
Simulated Annealing optimiser, implementing the classic temperature-based
probabilistic optimisation method.
This method uses a temperature schedule to control the probability of accepting
worse solutions as it explores the parameter space. As the temperature decreases,
the algorithm becomes more selective, eventually converging to a local or global
optimum.
The probability of accepting a worse solution is given by::
.. math::
P(accept) = exp(-(f_{\text{new}} - fold)/T)
The temperature decreases according to the cooling schedule::
.. math::
T = T0 * \alpha^k
where:
- :math: T0 is the initial temperature
- :math: \alpha is the cooling rate (between 0 and 1)
- :math: k is the iteration number
Parameters
----------
x0 : numpy array
Initial position
sigma0 : float
Initial step size
boundaries : dict, optional
Optional boundaries for parameters
"""
def __init__(self, x0, sigma0=0.05, boundaries=None):
super().__init__(x0, sigma0, boundaries)
# Set optimiser state
[docs]
self._ready_for_tell = False
# Best solution found
[docs]
self._x_best = self._x0
# Current point, score, and proposal
[docs]
self._current = self._x0
[docs]
self._current_f = np.inf
[docs]
self._proposed = self._x0
self._proposed.setflags(write=False)
# Temperature parameters
[docs]
self._temperature = 1.0
[docs]
self._temperature_decay = 0.95
[docs]
def ask(self):
"""
Returns a list of next points in the parameter-space
to evaluate from the optimiser.
"""
# Update temperature
self._temperature *= self._temperature_decay
# Generate new point with random perturbation
step = np.random.normal(0, self._sigma0, size=len(self._current))
self._proposed = self._current + step
# Apply boundaries
if self._boundaries is not None:
self._proposed = np.clip(
self._proposed, self._boundaries.lower(), self._boundaries.upper()
)
# Update state
self._ready_for_tell = True
self._running = True
return [np.array(self._proposed, copy=True)]
[docs]
def tell(self, reply):
"""
Receives a list of function values from the cost function from points
previously specified by `self.ask()`, and updates the optimiser state
accordingly.
"""
if not self._ready_for_tell:
raise RuntimeError("ask() must be called before tell().")
self._ready_for_tell = False
# Unpack reply
fx = reply[0]
# Accept or reject based on temperature and score.
# Always accept improved positions, probabilistically
# accept worse solutions
if fx < self._current_f:
accept = True
else:
p = np.exp(
-(fx - self._current_f) / (np.finfo(float).eps + self._temperature)
)
accept = np.random.random() < p
if accept:
self._current = np.array(self._proposed, copy=True)
self._current_f = fx
# Update best if current is best
if fx < self._f_best:
self._f_best = fx
self._x_best = np.array(self._current, copy=True)
self._iterations += 1
[docs]
def name(self):
"""
Returns the name of this optimiser.
"""
return "Simulated Annealing"
[docs]
def needs_sensitivities(self):
"""
Returns whether this method needs sensitivities.
"""
return False
[docs]
def n_hyper_parameters(self):
"""
Returns the number of hyper-parameters for this optimiser.
"""
return 2
[docs]
def running(self):
"""
Returns whether the optimisation is still running.
"""
return self._running
[docs]
def x_best(self):
"""
Returns the best position found.
"""
return self._x_best
[docs]
def f_best(self):
"""
Returns the best score found.
"""
return self._f_best
@property
[docs]
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, temp):
"""
Sets the temperature attribute, to be used
for initialisation before optimisation occurs.
"""
if not isinstance(temp, (int, float)):
raise TypeError("Temperature must be a number")
if temp < 0.0:
raise ValueError("Temperature must be positive")
self._temperature = float(temp)
@property
[docs]
def cooling_rate(self):
return self._temperature_decay
@cooling_rate.setter
def cooling_rate(self, alpha):
"""
Sets the cooling rate for the temperature schedule.
"""
if not isinstance(alpha, (int, float)):
raise TypeError("Cooling rate must be a number")
if not 0 < alpha < 1:
raise ValueError("Cooling rate must be between 0 and 1")
self._temperature_decay = float(alpha)