import numpy as np
from scipy.optimize import differential_evolution, minimize
from .base_optimiser import BaseOptimiser
[docs]
class SciPyMinimize(BaseOptimiser):
"""
Adapts SciPy's minimize function for use as an optimization strategy.
This class provides an interface to various scalar minimization algorithms implemented in SciPy, allowing fine-tuning of the optimization process through method selection and option configuration.
Parameters
----------
method : str, optional
The type of solver to use. If not specified, defaults to 'Nelder-Mead'.
Options: 'Nelder-Mead', 'Powell', 'CG', 'BFGS', 'Newton-CG', 'L-BFGS-B', 'TNC', 'COBYLA', 'SLSQP', 'trust-constr', 'dogleg', 'trust-ncg', 'trust-exact', 'trust-krylov'.
bounds : sequence or ``Bounds``, optional
Bounds for variables as supported by the selected method.
maxiter : int, optional
Maximum number of iterations to perform.
"""
def __init__(self, method=None, bounds=None, maxiter=None, tol=1e-5):
super().__init__()
self.method = method
self.bounds = bounds
self.tol = tol
self.options = {}
self._max_iterations = maxiter
if self.method is None:
self.method = "Nelder-Mead"
[docs]
def _runoptimise(self, cost_function, x0):
"""
Executes the optimization process using SciPy's minimize function.
Parameters
----------
cost_function : callable
The objective function to minimize.
x0 : array_like
Initial guess for the parameters.
Returns
-------
tuple
A tuple (x, final_cost) containing the optimized parameters and the value of `cost_function` at the optimum.
"""
self.log = [[x0]]
self.options = {"maxiter": self._max_iterations}
# Add callback storing history of parameter values
def callback(x):
self.log.append([x])
# Scale the cost function and eliminate nan values
self.cost0 = cost_function(x0)
self.inf_count = 0
if np.isinf(self.cost0):
raise Exception("The initial parameter values return an infinite cost.")
def cost_wrapper(x):
cost = cost_function(x) / self.cost0
if np.isinf(cost):
self.inf_count += 1
cost = 1 + 0.9**self.inf_count # for fake finite gradient
return cost
# Reformat bounds
if self.bounds is not None:
bounds = (
(lower, upper)
for lower, upper in zip(self.bounds["lower"], self.bounds["upper"])
)
result = minimize(
cost_wrapper,
x0,
method=self.method,
bounds=bounds,
tol=self.tol,
options=self.options,
callback=callback,
)
return result
[docs]
def needs_sensitivities(self):
"""
Determines if the optimization algorithm requires gradient information.
Returns
-------
bool
False, indicating that gradient information is not required.
"""
return False
[docs]
def name(self):
"""
Provides the name of the optimization strategy.
Returns
-------
str
The name 'SciPyMinimize'.
"""
return "SciPyMinimize"
[docs]
class SciPyDifferentialEvolution(BaseOptimiser):
"""
Adapts SciPy's differential_evolution function for global optimization.
This class provides a global optimization strategy based on differential evolution, useful for problems involving continuous parameters and potentially multiple local minima.
Parameters
----------
bounds : sequence or ``Bounds``
Bounds for variables. Must be provided as it is essential for differential evolution.
strategy : str, optional
The differential evolution strategy to use. Defaults to 'best1bin'.
maxiter : int, optional
Maximum number of iterations to perform. Defaults to 1000.
popsize : int, optional
The number of individuals in the population. Defaults to 15.
"""
def __init__(
self, bounds=None, strategy="best1bin", maxiter=1000, popsize=15, tol=1e-5
):
super().__init__()
self.tol = tol
self.strategy = strategy
self._max_iterations = maxiter
self._population_size = popsize
if bounds is None:
raise ValueError("Bounds must be specified for differential_evolution.")
elif not all(
np.isfinite(value) for sublist in bounds.values() for value in sublist
):
raise ValueError("Bounds must be specified for differential_evolution.")
elif isinstance(bounds, dict):
bounds = [
(lower, upper) for lower, upper in zip(bounds["lower"], bounds["upper"])
]
self.bounds = bounds
[docs]
def _runoptimise(self, cost_function, x0=None):
"""
Executes the optimization process using SciPy's differential_evolution function.
Parameters
----------
cost_function : callable
The objective function to minimize.
x0 : array_like, optional
Ignored parameter, provided for API consistency.
Returns
-------
tuple
A tuple (x, final_cost) containing the optimized parameters and the value of ``cost_function`` at the optimum.
"""
self.log = []
if x0 is not None:
print(
"Ignoring x0. Initial conditions are not used for differential_evolution."
)
# Add callback storing history of parameter values
def callback(x, convergence):
self.log.append([x])
result = differential_evolution(
cost_function,
self.bounds,
strategy=self.strategy,
maxiter=self._max_iterations,
popsize=self._population_size,
tol=self.tol,
callback=callback,
)
return result
[docs]
def set_population_size(self, population_size=None):
"""
Sets a population size to use in this optimisation.
Credit: PINTS
"""
# Check population size or set using heuristic
if population_size is not None:
population_size = int(population_size)
if population_size < 1:
raise ValueError("Population size must be at least 1.")
self._population_size = population_size
[docs]
def needs_sensitivities(self):
"""
Determines if the optimization algorithm requires gradient information.
Returns
-------
bool
False, indicating that gradient information is not required for differential evolution.
"""
return False
[docs]
def name(self):
"""
Provides the name of the optimization strategy.
Returns
-------
str
The name 'SciPyDifferentialEvolution'.
"""
return "SciPyDifferentialEvolution"