import warnings
from abc import ABC, abstractmethod
from time import sleep
from typing import Sequence, Tuple, Union
import numpy as np
__all__ = ("BaseTestCase",
"Ackley",
"Alpine01",
"Alpine02",
"Deceptive",
"Easom",
"ExpLeastSquaresCost",
"Griewank",
"Langermann",
"LennardJones",
"Levy",
"Michalewicz",
"Qing",
"Rana",
"Rastrigin",
"Rosenbrock",
"Schwefel",
"Shekel",
"Shubert",
"Stochastic",
"StyblinskiTang",
"Trigonometric",
"Vincent",
"ZeroSum",
)
[docs]class BaseTestCase(ABC):
""" Basic API for Optimization test cases.
Parameters
----------
dims
Number of parameters in the input space.
delay
Pause (in seconds) between function evaluations to mimic slow functions.
"""
def __init__(self, dims: int, *, delay: float = 0):
self._dims = dims
self._delay = delay
@property
def dims(self) -> int:
""" Number of parameters in the input space. """
return self._dims
@property
@abstractmethod
def min_x(self) -> Sequence[float]:
""" The location of the global minimum in parameter space. """
@property
@abstractmethod
def min_fx(self) -> float:
""" The function value of the global minimum. """
@property
@abstractmethod
def bounds(self) -> Sequence[Tuple[float, float]]:
""" Sequence of min/max pairs bounding the function in each dimension. """
@property
def delay(self) -> float:
""" Delay (in seconds) between function evaluations to mimic slow functions. """
return self._delay
[docs] @abstractmethod
def __call__(self, x: Sequence[float]) -> float:
""" Evaluates the function.
Parameters
----------
x
Vector in parameter space where the function will be evaluated.
Returns
-------
float
Function value at `x`.
"""
sleep(self.delay)
[docs]class Ackley(BaseTestCase):
""" Implementation of the Ackley optimization test function [b]_.
.. math::
f(x) = - a \\exp\\left(-b \\sqrt{\\frac{1}{d}\\sum^d_{i=1}x_i^2}\\right)
- \\exp\\left(\\frac{1}{d}\\sum^d_{i=1}\\cos\\left(cx_i\\right)\\right)
+ a
+ \\exp(1)
Recommended bounds: :math:`x_i \\in [-32.768, 32.768]`
Global minimum: :math:`f(0, 0, ..., 0) = 0`
.. image:: /_static/ackley.png
:align: center
:alt: Multimodal flat surface with a single deep global minima. Multimodal version of the Easom function.
Parameters
----------
a
Ackley function parameter
b
Ackley function parameter
c
Ackley function parameter
"""
def __init__(self, dims: int = 2, a: float = 20, b: float = 0.2, c: float = 2 * np.pi, *, delay: float = 0):
super().__init__(dims, delay=delay)
self.a, self.b, self.c = a, b, c
def __call__(self, x) -> float:
x = np.array(x)
term1 = -self.a
sos = 1 / self.dims * np.sum(x ** 2)
term1 *= np.exp(-self.b * np.sqrt(sos))
cos = 1 / self.dims * np.sum(np.cos(self.c * x))
term2 = -np.exp(cos)
term34 = self.a + np.e
sleep(self.delay)
return term1 + term2 + term34
@property
def min_x(self) -> Sequence[float]:
return [0] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [(-32.768, 32.768)] * self.dims
[docs]class Alpine01(BaseTestCase):
""" Implementation of the Alpine Type-I optimization test function [a]_.
.. math::
f(x) = \\sum^n_{i=1}\\left|x_i\\sin\\left(x_i\\right)+0.1x_i\\right|
Recommended bounds: :math:`x_i \\in [-10, 10]`
Global minimum: :math:`f(0, 0, ..., 0) = 0`
.. image:: /_static/alpine01.png
:align: center
:alt: Highly oscillatory non-periodic surface.
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
calc = np.sin(x)
calc *= x
calc += 0.1 * x
calc = np.abs(calc)
return np.sum(calc)
@property
def min_x(self) -> Sequence[float]:
return [0] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [(-10, 10)] * self.dims
[docs]class Alpine02(BaseTestCase):
""" Implementation of the Alpine Type-II optimization test function [a]_.
.. math::
f(x) = - \\prod_{i=1}^n \\sqrt{x_i} \\sin{x_i}
Recommended bounds: :math:`x_i \\in [0, 10]`
Global minimum: :math:`f(7.917, 7.917, ..., 7.917) = -6.1295`
.. image:: /_static/alpine02.png
:align: center
:alt: Moderately oscillatory periodic surface.
"""
def __call__(self, x) -> float:
super().__call__(x)
calc = np.sin(x)
calc *= np.sqrt(x)
return -np.prod(calc)
@property
def min_x(self) -> Sequence[float]:
return [7.917] * self.dims
@property
def min_fx(self) -> float:
return -2.808 ** self.dims
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[0, 10]] * self.dims
[docs]class Deceptive(BaseTestCase):
""" Implementation of the Deceptive optimization test function [a]_.
.. math:
f(x) = - \\left[\\frac{1}{n}\\sum^n_{i=1}g_i\\left(x_i\\right)\\right]
Recommended bounds: :math:`x_i \\in [0, 1]`
Global minimum: :math:`f(a) = -1`
.. image:: /_static/deceptive.png
:align: center
:alt: Small global minimum surrounded by areas which slope away from it.
Parameters
----------
b
Non-linearity parameter.
shift_positive
Shifts the entire function such that the global minimum falls at 0.
"""
def __init__(self, dims: int = 2, b: float = 2, *, shift_positive: bool = False, delay: float = 0):
super().__init__(dims, delay=delay)
self.shift = shift_positive
self.b = b
self._min_x = np.random.uniform(0, 1, dims)
def __call__(self, x):
sleep(self.delay)
x = np.array(x)
calc = - (1 / self.dims * np.sum(self._g(x))) ** self.b
if self.shift:
return calc + 1
return calc
def _g(self, vec: np.ndarray):
""" Sub-calculation of the :meth:`__call__` method which returns :math:`g(x)`. """
gx = np.zeros(len(vec))
for i, x in enumerate(vec):
ai = self._min_x[i]
if 0 <= x <= 0.8 * ai:
gx[i] = 0.8 - x / ai
elif 0.8 * ai < x <= ai:
gx[i] = 5 * x / ai - 4
elif ai < x <= (1 + 4 * ai) / 5:
gx[i] = (5 * (x - ai)) / (ai - 1) + 1
elif (1 + 4 * ai) / 5 < x <= 1:
gx[i] = (x - 1) / (1 - ai) + 0.8
return gx
@property
def min_x(self) -> Sequence[float]:
return self._min_x
@property
def min_fx(self) -> float:
return -1 if not self.shift else 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[0, 1]] * self.dims
[docs]class Easom(BaseTestCase):
""" Implementation of the Easom optimization test function [a]_.
.. math::
f(x) = - \\cos\\left(x_1\\right)\\cos\\left(x_2\\right)\\exp\\left(-(x_1-\\pi)^2-(x_2-\\pi)^2\\right)
Recommended bounds: :math:`x_1,x _2 \\in [-100, 100]`
Global minimum: :math:`f(\\pi, \\pi) = -1`
.. image:: /_static/easom.png
:align: center
:alt: Totally flat surface with a single very small bullet hole type minimum.
Parameters
----------
shift_positive
Shifts the entire function such that the global minimum falls at 0.
"""
def __init__(self, *args, shift_positive: bool = False, delay: float = 0):
super().__init__(2, delay=delay)
self.shift = shift_positive
def __call__(self, x):
sleep(self.delay)
calc = -np.cos(x[0])
calc *= np.cos(x[1])
calc *= np.exp(-(x[0] - np.pi) ** 2 - (x[1] - np.pi) ** 2)
if self.shift:
return calc + 1
return calc
@property
def min_x(self) -> Sequence[float]:
return [np.pi, np.pi]
@property
def min_fx(self) -> float:
return -1 if not self.shift else 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-100, 100]] * 2
[docs]class Griewank(BaseTestCase):
""" Implementation of the Griewank optimization test function [b]_.
.. math::
f(x) = \\sum_{i=1}^d \\frac{x_i^2}{4000} - \\prod_{i=1}^d \\cos\\left(\\frac{x_i}{\\sqrt{i}}\\right) + 1
Recommended bounds: :math:`x_i \\in [-600, 600]`
Global minimum: :math:`f(0, 0, ..., 0) = 0`
.. image:: /_static/griewank.png
:align: center
:alt: Highly oscillatory totally-periodic surface on a general parabolic surface. Similar to Rastrigin.
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
term1 = 1 / 4000 * np.sum(x ** 2)
term2 = x / np.sqrt(np.arange(1, len(x) + 1))
term2 = np.prod(np.cos(term2))
return 1 + term1 - term2
@property
def min_x(self) -> Sequence[float]:
return [0] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-600, 600]] * self.dims
[docs]class Langermann(BaseTestCase):
""" When called returns evaluations of the Langermann function [a]_ [b]_.
.. math::
f(x) & = & - \\sum_{i=1}^5 \\frac{c_i\\cos\\left(\\pi\\left[(x_1-a_i)^2 + (x_2-b_i)^2\\right]\\right)}
{\\exp\\left(\\frac{(x_1-a_i)^2 + (x_2-b_i)^2}{\\pi}\\right)}\\\\
\\mathbf{a} & = & \\{3, 5, 2, 1, 7\\}\\\\
\\mathbf{b} & = & \\{5, 2, 1, 4, 9\\}\\\\
\\mathbf{c} & = & \\{1, 2, 5, 2, 3\\}\\\\
Recommended bounds: :math:`x_1, x_2 \\in [0, 10]`
Global minimum: :math:`f(2.00299219, 1.006096) = -5.1621259`
.. image:: /_static/langermann.png
:align: center
:alt: Analogous to ripples on a water surface after three drops have hit it.
Parameters
----------
shift_positive
Shifts the entire function such that the global minimum falls at ~0.
"""
def __init__(self, *args, shift_positive: bool = False, delay: float = 0):
super().__init__(2, delay=delay)
self.shift = shift_positive
def __call__(self, x: np.ndarray) -> float:
a = np.array([3, 5, 2, 1, 7])
b = np.array([5, 2, 1, 4, 9])
c = np.array([1, 2, 5, 2, 3])
x1, x2 = x[0], x[1]
cos = c * np.cos(np.pi * ((x1 - a) ** 2 + (x2 - b) ** 2))
exp = np.exp(((x1 - a) ** 2 + (x2 - b) ** 2) / np.pi)
sleep(self.delay)
calc = - np.sum(cos / exp)
if self.shift:
return calc + 5.2
return calc
@property
def min_x(self) -> Sequence[float]:
return [2.00299219, 1.006096]
@property
def min_fx(self) -> float:
return -5.1621259 if not self.shift else 0.0378741
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[0, 10]] * 2
[docs]class ExpLeastSquaresCost(BaseTestCase):
""" Least squares type cost function.
Bespoke test function which takes the form of least squares cost function by solving for the parameters of a
sum of exponential terms. Compatible with the GFLS solver.
.. math::
f(p) & = & \\sum_i^{n} (g - g_{train})^2\\\\
g(p, u) & = & \\sum_i^d \\exp(-p_i u)\\\\
g_{train}(p) & = & g(p, u_{train}) \\\\
u_{train} & = & \\mathcal{U}_{[x_{min}, x_{max}]}
Recommended bounds: :math:`x_i \\in [-2, 2]`
Global minimum: :math:`f(p_1, p_2, ..., p_n) \\approx 0`
.. image:: /_static/lstsqrs.png
:align: center
:alt: Minimum sandwiched between very flat surface and very steep walls.
Parameters
----------
n_train
Number of training points used in the construction of the error function.
sigma_eval
Random perturbations added at the execution of each function evaluation.
:math:`f = f(1 + \\mathcal{U}_{[-\\sigma_{eval}, \\sigma_{eval}]})`
sigma_fixed
Random perturbations added to the construction of the training set so that the
global minimum error cannot be zero.
:math:`g_{train} = g_{train}(1 + \\mathcal{U}_{[-\\sigma_{eval}, \\sigma_{eval}]})`
u_train
If an int is provided, training points are randomly selected in the interval :math:`[0, u_{train})`.
If a tuple is provided, training points are randomly selected in the interval :math:`[u_{train,0},
u_{train,1}]`.
If an array like object of length >2 is provided then the list is explicitly used as the locations of
the training points.
p_range
Range between which the true parameter values will be drawn.
Notes
-----
The properties :attr:`~BaseTestCase.min_fx` and :attr:`~BaseTestCase.min_x` are only guaranteed for
`sigma_fixed` = 0; otherwise they are only estimates. This is because the added random noise may create a
better fit of the data an unknown vector.
"""
def __init__(self, dims: int = 2, n_train: int = 10, sigma_eval: float = 0,
sigma_fixed: float = 0, u_train: Union[int, Tuple[float, float], Sequence[float]] = 10,
p_range: Tuple[float, float] = (-2, 2), *, delay: float = 0):
super().__init__(dims, delay=delay)
self.n_train = n_train
self.sigma_eval = sigma_eval
self.sigma_fixed = sigma_fixed
self.p_range = p_range
if isinstance(u_train, int):
self.u_train = np.random.uniform(0, u_train, n_train)
elif len(u_train) == 2:
self.u_train = np.random.uniform(u_train[0], u_train[1], n_train)
else:
self.u_train = np.array(u_train)
self.p_true = np.random.uniform(p_range[0], p_range[1], dims)
g_train_core = np.sum(np.exp(np.einsum('j,h->hj', -self.p_true, self.u_train)), axis=1)
self.g_train = g_train_core * (1 + np.random.uniform(-self.sigma_fixed, self.sigma_fixed, self.n_train))
self._min_fx = sum((g_train_core - self.g_train) ** 2)
def __call__(self, x) -> float:
x = np.array(x)
return np.sum(self.detailed_call(x) ** 2)
[docs] def detailed_call(self, x) -> Sequence[float]:
""" Returns a sequence of contributions which are squared and summed to get the final function value. """
super().__call__(x)
g_x = np.sum(np.exp(np.einsum('j,h->hj', -x, self.u_train)), axis=1)
error = g_x - self.g_train
error *= 1 + np.random.uniform(-self.sigma_eval, self.sigma_eval)
return error
@property
def min_x(self) -> Sequence[float]:
return self.p_true
@property
def min_fx(self) -> float:
return self._min_fx
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [list(self.p_range)] * self.dims
[docs]class LennardJones(BaseTestCase):
""" Lennard-Jones energy potential function.
Designed to predict the energy for clusters of atoms, this potential energy surface is characterized by steep
cliffs, infinite values, and degenerate local and global minima.
The input vector :math:`\\mathbf{x}` is reshaped into an :math:`N \\times d` array (:math:`X`) of
:math:`d`-dimensional Cartesian coordinates for :math:`N` particles.
.. math::
f(X) = 4\\epsilon\\sum_{i<j}{\\left[\\left(\\frac{\\sigma}{r_{ij}}\\right)^{12} -
\\left(\\frac{\\sigma}{r_{ij}}\\right)^6\\right]}
where :math:`\\epsilon` and :math:`\\sigma` are parameters and :math:`r_{ij}` is the Euclidean distance between
particles :math:`i` and :math:`j`.
Recommended bounds: :math:`x_i \\in [-2^{-1/6}\\sigma\\sqrt[3]{\\frac{\\pi}{3N}}, 2^{-1/6}\\sigma\\sqrt[3]{\\frac{\\pi}{3N}}]`
Global minimum: Estimated from :math:`N` and :math:`d`
.. image:: /_static/lennardjones.png
:align: center
:alt: Global minima sandwiched between infinite values, flat surfaces and many local minima.
Parameters
----------
atoms
The number of particles (:math:`N`).
dims
The number of Cartesian spatial dimensions (:math:`d`).
eps
The magnitude parameter (:math:`\\epsilon`).
sigma
The shape parameter (:math:`\\sigma`).
Attributes
----------
dims
The number of adjustable parameters in the optimization problem is :math:`Nd`.
Notes
-----
The `x` parameter for the call method may be a :math:`Nd` length vector or a :math:`N \\times d` array.
"""
def __init__(self, atoms: int, dims: int, eps: float = 1, sigma: float = 1, *, delay=None):
super().__init__(atoms * dims, delay=delay)
self.N = atoms
self.d = dims
self.global_min_fitting_coefficients = np.array([-4.11622788900031E-11,
2.73806188513917E-08,
-7.29613541449689E-06,
0.000981156639160743,
-0.0747067533967315,
-3.00175089914711,
7.06759211566204])
self.eps = eps
self.sig = sigma
bnd = self.sig * np.cbrt(np.pi / 3 / np.sqrt(2) * self.N)
self._bounds = [(-bnd, bnd)] * self.dims
# Pre-calculation shortcuts
self._triu_indices = np.triu_indices(atoms, 1)
self._sig2 = sigma ** 2
self._sig5 = sigma ** 5
self._sig11 = sigma ** 11
def __call__(self, x: Sequence[float]) -> float:
x, delta, dists_sq = self._common_calc(x)
calc = self._sig2 / np.array(dists_sq)
calc = calc ** 6 - calc ** 3
calc = calc.sum()
calc *= 4 * self.eps
return calc
[docs] def jacobian(self, x: Sequence[float]) -> np.ndarray:
""" Returns the gradient (first derivative) of the function in all directions, calculated analytically.
Parameters
----------
x
Vector in parameter space where the function will be evaluated.
Returns
-------
numpy.ndarray
Vector of derivatives for each dimension in `x`.
"""
x, delta, dists_sq = self._common_calc(x)
energy_grads = np.einsum(
"n,nd->nd",
-24 * self.eps * (2 * self._sig11 * dists_sq ** -7 - self._sig5 * dists_sq ** -4),
delta
)
gradient = np.zeros_like(x)
np.add.at(gradient, self._triu_indices[0], energy_grads)
np.add.at(gradient, self._triu_indices[1], -energy_grads)
return gradient.ravel()
def _common_calc(self, x: Sequence[float]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
""" Core calculation components used by both the core function and its derivative.
Returns
-------
numpy.ndarray
Processed and correctly shaped `x'.
numpy.ndarray
Array of differences between points in `x`.
numpy.ndarray
Array of square distances between points in `x`.
"""
x = np.array(x)
if x.ndim == 1:
x = x.reshape((self.N, self.d))
delta = x[self._triu_indices[0]] - x[self._triu_indices[1]]
dists_sq = np.einsum("nd,nd->n", delta, delta)
dists_sq[dists_sq == 0] = np.inf
return x, delta, dists_sq
@property
def min_x(self) -> Sequence[float]:
warnings.warn("Location of minima unknown.")
return [0] * self.dims
@property
def min_fx(self) -> float:
if self._dims == 3:
warnings.warn("Energy minimum estimate only.")
energy = self.global_min_fitting_coefficients * self.N ** np.arange(6, -1, -1)
return energy.sum()
else:
warnings.warn("Energy minimum estimate only provided in 3D space.")
return np.nan
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return self._bounds
[docs]class Levy(BaseTestCase):
""" Implementation of the Levy optimization test function [b]_.
.. math::
f(x) & = & \\sin^2(\\pi w_1) + \\sum^{d-1}_{i=1}\\left(w_i-1\\right)^2\\left[1+10\\sin^2\\left(\\pi w_i +1
\\right)\\right] + \\left(w_d-1\\right)^2\\left[1+\\sin^2\\left(2\\pi w_d\\right)\\right] \\\\
w_i & = & 1 + \\frac{x_i - 1}{4}
Recommended bounds: :math:`x_i \\in [-10, 10]`
Global minimum: :math:`f(1, 1, ..., 1) = 0`
.. image:: /_static/levy.png
:align: center
:alt: Moderately oscillatory periodic surface.
"""
def __call__(self, x: np.ndarray) -> float:
x = np.array(x)
w = self._w(x)
term1 = np.sin(np.pi * w[0]) ** 2
term2 = (w - 1) ** 2
term2 *= 1 + 10 * np.sin(np.pi * w + 1) ** 2
term2 = np.sum(term2)
term3 = (w[-1] - 1) ** 2
term3 *= 1 + np.sin(2 * np.pi * w[-1]) ** 2
sleep(self.delay)
return term1 + term2 + term3
@staticmethod
def _w(x: np.ndarray) -> np.ndarray:
return 1 + (x - 1) / 4
@property
def min_x(self) -> Sequence[float]:
return [1] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-10, 10]] * self.dims
[docs]class Michalewicz(BaseTestCase):
""" Implementation of the Michalewicz optimization test function [b]_.
.. math::
f(x) = - \\sum^d_{i=1}\\sin(x_i)\\sin^{2m}\\left(\\frac{ix_i^2}{\\pi}\\right)
Recommended bounds: :math:`x_i \\in [0, \\pi]`
Global minimum:
.. math::
f(x) = \\begin{cases}
-1.8013 & \\text{if} & d=2 \\\\
-4.687 & \\text{if} & d=5 \\\\
-9.66 & \\text{if} & d=10 \\\\
\\end{cases}
.. image:: /_static/michalewicz.png
:align: center
:alt: Flat surface with many valleys and a single global minimum.
Parameters
----------
m
Parametrization of the function. Lower values make the valleys more informative at pointing to the
minimum. High values (:math:`\\pm10`) create a needle-in-a-haystack function where there is no
information pointing to the minimum.
"""
def __init__(self, dims: int = 2, m: float = 10, *, delay: float = 0):
super().__init__(dims, delay=delay)
self.m = m
def __call__(self, x):
sleep(self.delay)
i = np.arange(1, len(x) + 1)
x = np.array(x)
calc = np.sin(x)
calc *= np.sin(i * x ** 2 / np.pi) ** (2 * self.m)
return -np.sum(calc)
@property
def min_x(self) -> Sequence[float]:
warnings.warn("Global minimum only known for d=2, 5 and 10 but locations are unknown.")
return [0] * self._dims
@property
def min_fx(self) -> float:
if self._dims == 2:
return -1.8013
if self._dims == 5:
return -4.687658
if self._dims == 10:
return -9.66015
warnings.warn("Global minimum only known for d=2, 5 and 10")
return np.inf
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [(0, np.pi)] * self.dims
[docs]class Qing(BaseTestCase):
""" Implementation of the Qing optimization test function [a]_.
.. math::
f(x) = \\sum^d_{i=1} (x_i^2-i)^2
Recommended bounds: :math:`x_i \\in [-500, 500]`
Global minimum: :math:`f(\\sqrt{1}, \\sqrt{2}, ..., \\sqrt{n}) = 0`
.. image:: /_static/qing.png
:align: center
:alt: Globally flat with parabolic walls but has :math:`2^d` degenerate global minima.
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
i = np.arange(1, self.dims + 1)
calc = (x ** 2 - i) ** 2
return np.sum(calc)
@property
def min_x(self) -> Sequence[float]:
warnings.warn("Global minimum is degenerate at every positive and negative combination of the returned "
"parameter vector.", UserWarning)
return np.sqrt(np.arange(1, self.dims + 1))
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-500, 500]] * self.dims
[docs]class Rana(BaseTestCase):
""" Implementation of the Rana optimization test function [a]_.
.. math::
f(x) = \\sum^d_{i=1}\\left[x_i\\sin\\left(\\sqrt{\\left|x_1-x_i+1\\right|}\\right)
\\cos\\left(\\sqrt{\\left|x_1+x_i+1\\right|}\\right)\\\\
+ (x_1+1)\\sin\\left(\\sqrt{\\left|x_1+x_i+1\\right|}\\right)
\\cos\\left(\\sqrt{\\left|x_1-x_i+1\\right|}\\right)
\\right]
Recommended bounds: :math:`x_i \\in [-500.000001, 500.000001]`
Global minimum: :math:`f(-500, -500, ..., -500) = -928.5478`
.. image:: /_static/rana.png
:align: center
:alt: Highly multimodal and chaotic, optimum is on the lower bound
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
term1 = x
term1 = term1 * np.sin(np.sqrt(np.abs(x[0] - x + 1)))
term1 = term1 * np.cos(np.sqrt(np.abs(x[0] + x + 1)))
term2 = x[0] + 1
term2 = term2 * np.sin(np.sqrt(np.abs(x[0] + x + 1)))
term2 = term2 * np.cos(np.sqrt(np.abs(x[0] - x + 1)))
return np.sum(term1 + term2)
@property
def min_x(self) -> Sequence[float]:
return [-500] * self.dims
@property
def min_fx(self) -> float:
return self.__call__(self.min_x)
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-500.000001, 500.000001]] * self.dims
[docs]class Rastrigin(BaseTestCase):
""" Implementation of the Rastrigin optimization test function [b]_.
.. math::
f(x) = 10d + \\sum^d_{i=1} \\left[x_i^2-10\\cos(2\\pi x_i)\\right]
Recommended bounds: :math:`x_i \\in [-5.12, 5.12]`
Global minimum: :math:`f(0, 0, ..., 0) = 0`
.. image:: /_static/rastrigin.png
:align: center
:alt: Modulation of a unimodal paraboloid with multiple regular local minima.
"""
def __call__(self, x):
x = np.array(x)
calc = 10 * self.dims
calc += np.sum(x ** 2 - 10 * np.cos(2 * np.pi * x))
sleep(self.delay)
return calc
@property
def min_x(self) -> Sequence[float]:
return [0] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-5.12, 5.12]] * self.dims
[docs]class Rosenbrock(BaseTestCase):
""" Implementation of the Rosenbrock optimization test function [b]_.
.. math::
f(x) = \\sum^{d-1}_{i=1}\\left[100(x_{i+1}-x_i^2)^2+(x_i-1)^2\\right]
Recommended bounds: :math:`x_i \\in [-2.048, 2.048]`
Global minimum: :math:`f(1, 1, ..., 1) = 0`
.. image:: /_static/rosenbrock.png
:align: center
:alt: Global minimum is located in a very easy to find valley but locating it within the valley is difficult.
"""
def __call__(self, x):
total = 0
for i in range(self.dims - 1):
total += 100 * (x[i + 1] - x[i] ** 2) ** 2 + (1 - x[i]) ** 2
sleep(self.delay)
return total
@property
def min_x(self) -> Sequence[float]:
return [1] * self.dims
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-2.048, 2.048]] * self._dims
[docs]class Schwefel(BaseTestCase):
""" Implementation of the Schwefel optimization test function [b]_.
.. math::
f(x) = 418.9829d - \\sum^d_{i=1} x_i\\sin\\left(\\sqrt{|x_i|}\\right)
Recommended bounds: :math:`x_i \\in [-500, 500]`
Global minimum: :math:`f(420.9687, 420.9687, ..., 420.9687) = -418.9829d`
.. image:: /_static/schwefel.png
:align: center
:alt: Multimodal and deceptive in that the global minimum is very far from the next best local minimum.
Parameters
----------
shift_positive
Shifts the entire function such that the global minimum falls at ~0.
"""
def __init__(self, dims: int = 2, *, shift_positive: bool = False, delay: float = 0):
super().__init__(dims, delay=delay)
self.shift = shift_positive
def __call__(self, x):
sleep(self.delay)
x = np.array(x)
calc = np.sum(-x * np.sin(np.sqrt(np.abs(x))))
if self.shift:
return calc + 418.9830 * self.dims
return calc
@property
def min_x(self) -> Sequence[float]:
return [420.9687] * self.dims
@property
def min_fx(self) -> float:
return -418.9829 * self.dims if not self.shift else 0.0001 * self.dims
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-500, 500]] * self.dims
[docs]class Shekel(BaseTestCase):
""" Implementation of the Shekel optimization test function [b]_.
.. math::
f(x) = - \\sum^m_{i=1}\\left(\\sum^d_{j=1} (x_j - C_{ji})^2 + \\beta_i\\right)^{-1}
Recommended bounds: :math:`x_i \\in [-32.768, 32.768]`
Global minimum: :math:`f(4, 4, 4, 4) =~ -10`
.. image:: /_static/shekel.png
:align: center
:alt: Multiple minima of different depths clustered together on a mostly-flat surface.
Parameters
----------
m
Number of minima. Global minimum certified for m=5,7 and 10.
shift_positive
Shifts the entire function such that the function is strictly positive.
Since this is variable for this function the adjustment is +12 and thus the global minimum will not
necessarily fall at zero.
"""
def __init__(self, dims: int = 2, m: int = 10, *, shift_positive: bool = False, delay: float = 0):
assert 0 < dims < 5
super().__init__(dims, delay=delay)
self.shift = shift_positive
if any([m == i for i in (5, 7, 10)]):
self.m = m
self.a = np.array([[4] * 4,
[1] * 4,
[8] * 4,
[6] * 4,
[3, 7] * 2])
self.c = 0.1 * np.array([1, 2, 2, 4])
if m == 5:
self.c = np.append(self.c, 0.6)
else:
self.a = np.append(self.a,
np.array([[2, 9] * 2,
[5, 5, 3, 3]]),
axis=0)
self.c = np.append(self.c, 0.1 * np.array([4, 6, 3]))
if m == 10:
self.a = np.append(self.a,
np.array([[8, 1] * 2,
[6, 2] * 2,
[7, 3.6] * 2]),
axis=0)
self.c = np.append(self.c, 0.1 * np.array([7, 5, 5]))
else:
raise ValueError("m can only be 5, 7 or 10")
def __call__(self, x):
sleep(self.delay)
x = np.array(x)
calc = (self.c + np.sum((x - self.a[:, :self.dims]) ** 2, axis=1)) ** -1
calc = -np.sum(calc)
if self.shift:
return calc + 12
return calc
@property
def min_x(self) -> Sequence[float]:
return [4] * self.dims
@property
def min_fx(self) -> float:
warnings.warn("Global minimum is only known for some combinations of m and d. The provided value is "
"approximate.")
return -10.6
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-32.768, 32.768]] * self.dims
[docs]class Shubert(BaseTestCase):
""" Implementation of the Shubert Type-I, Type-III and Type-IV optimization test functions [a]_.
.. math::
f_I(x) & = & \\sum^2_{i=1}\\sum^5_{j=1} j \\cos\\left[(j+1)x_i+j\\right]\\\\
f_{III}(x) & = & \\sum^5_{i=1}\\sum^5_{j=1} j \\sin\\left[(j+1)x_i+j\\right]\\\\
f_{IV}(x) & = & \\sum^5_{i=1}\\sum^5_{j=1} j \\cos\\left[(j+1)x_i+j\\right]\\\\
Recommended bounds: :math:`x_i \\in [-10, 10]`
.. image:: /_static/shubert.png
:align: center
:alt: Highly oscillatory, periodic surface. Many degenerate global minima regularly placed.
Parameters
----------
style
Selection between the Shubert01, Shubert03 & Shubert04 functions. Each more oscillatory than the previous.
shift_positive
Shifts the entire function such that the global minimum falls at 0.
"""
def __init__(self, dims: int = 2, style: int = 1, *, shift_positive: bool = False, delay: float = 0):
super().__init__(dims, delay=delay)
self.style = style
self.shift_positive = shift_positive
def __call__(self, x) -> float:
super().__call__(x)
if self.style == 1:
x = np.array(x).reshape((-1, 1))
j = np.arange(1, 6)
calc = (j + 1) * x + j
calc = np.cos(calc)
calc = j * calc
calc = np.sum(calc, axis=1)
calc = np.prod(calc)
if self.shift_positive:
calc += 186.731
elif self.style == 3:
x = np.reshape(x, (-1, 1))
j = np.arange(1, 6)
calc = (j + 1) * x + j
calc = np.sin(calc)
calc = j * calc
calc = np.sum(calc)
if self.shift_positive:
calc += 24.062499
else:
x = np.reshape(x, (-1, 1))
j = np.arange(1, 6)
calc = (j + 1) * x + j
calc = np.cos(calc)
calc = j * calc
calc = np.sum(calc)
if self.shift_positive:
calc += 29.016015
return calc
@property
def min_x(self) -> Sequence[float]:
warnings.warn("Degenerate global minima")
return
@property
def min_fx(self) -> float:
if self._dims > 2:
warnings.warn("Minimum unknown for d>2")
return None
if self.shift_positive:
return 0
if self.style == 1:
return -186.7309
if self.style == 3:
return -24.062499
return -29.016015
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-10, 10]] * self._dims
[docs]class Stochastic(BaseTestCase):
""" Implementation of the Stochastic optimization test function [a]_.
.. math::
f(x) & = & \\sum^d_{i=1} \\epsilon_i\\left|x_i-\\frac{1}{i}\\right| \\\\
\\epsilon_i & = & \\mathcal{U}_{[0, 1]}
Recommended bounds: :math:`x_i \\in [-5, 5]`
Global minimum: :math:`f(1/d, 1/d, ..., 1/d) = 0`
.. image:: /_static/stochastic.png
:align: center
:alt: Parabolic function with random evaluation noise making a substantial contribution.
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
e = np.random.rand(self.dims)
i = np.arange(1, self.dims + 1)
calc = e * np.abs(x - 1 / i)
return np.sum(calc)
@property
def min_x(self) -> Sequence[float]:
return 1 / np.arange(1, self.dims + 1)
@property
def min_fx(self) -> float:
return 0
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-5, 5]] * self.dims
[docs]class StyblinskiTang(BaseTestCase):
""" Implementation of the Styblinski-Tang optimization test function [b]_.
.. math::
f(x) = \\frac{1}{2}\\sum^d_{i=1}\\left(x_i^4-16x_i^2+5x_i\\right)
Recommended bounds: :math:`x_i \\in [-500, 500]`
Global minimum: :math:`f(-2.90, -2.90, ..., -2.90) = -39.16616570377 d`
.. image:: /_static/styblinskitang.png
:align: center
:alt: Similar to Qing function but minima are deceptively similar but not actually degenerate.
"""
def __call__(self, x) -> float:
super().__call__(x)
x = np.array(x)
calc = x ** 4 - 16 * x ** 2 + 5 * x
return 0.5 * np.sum(calc)
@property
def min_x(self) -> Sequence[float]:
return [-2.903534018185960] * self.dims
@property
def min_fx(self) -> float:
return -39.16616570377142 * self.dims
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-500, 500]] * self.dims
[docs]class Trigonometric(BaseTestCase):
""" Implementation of the Trigonometric Type-II optimization test function [a]_.
.. math::
f(x) = 1 + \\sum_{i=1}^d 8 \\sin^2 \\left[7(x_i-0.9)^2\\right]
+ 6 \\sin^2 \\left[14(x_i-0.9)^2\\right]+(x_i-0.9)^2
Recommended bounds: :math:`x_i \\in [-500, 500]`
Global minimum: :math:`f(0.9, 0.9, ..., 0.9) = 1`
.. image:: /_static/trigonometric.png
:align: center
:alt: Parabolic but becomes a multimodal flat surface with many peaks and troughs near the minimum.
"""
def __call__(self, x) -> float:
super().__call__(x)
core = (np.array(x) - 0.9) ** 2
sin1 = 8 * np.sin(7 * core) ** 2
sin2 = 6 * np.sin(14 * core) ** 2
total = sin1 + sin2 + core
return 1 + np.sum(total)
@property
def min_x(self) -> Sequence[float]:
return [0.9] * self.dims
@property
def min_fx(self) -> float:
return 1
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-500, 500]] * self.dims
[docs]class Vincent(BaseTestCase):
""" Implementation of the Vincent optimization test function [a]_.
.. math::
f(x) = - \\sum^d_{i=1} \\sin\\left(10\\log(x)\\right)
Recommended bounds: :math:`x_i \\in [0.25, 10]`
Global minimum: :math:`f(7.706, 7.706, ..., 7.706) = -d`
.. image:: /_static/vincent.png
:align: center
:alt: 'Flat' surface made of period peaks and trough of various sizes.
"""
def __call__(self, x) -> float:
super().__call__(x)
calc = 10 * np.log(x)
calc = np.sin(calc)
calc = -np.sum(calc)
return calc
@property
def min_x(self) -> Sequence[float]:
return [7.70628098] * self.dims
@property
def min_fx(self) -> float:
return -self.dims
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[0.25, 10]] * self.dims
[docs]class ZeroSum(BaseTestCase):
""" Implementation of the ZeroSum optimization test function [a]_.
.. math::
f(x) = \\begin{cases}
0 & \text{if} \\sum^n_{i=1} x_i = 0 \\\\
1 + (10000|\\sum^n_{i=1} x_i = 0|)^{0.5} & \text{otherwise}
\\end{cases}
Recommended bounds: :math:`x_i \\in [-10, 10]`
Global minimum: :math:`f(x) = 0 \text{where} \\sum^n_{i=1} x_i = 0`
.. image:: /_static/zerosum.png
:align: center
:alt: Single valley of degenerate global minimum results that is not axi-parallel.
"""
def __call__(self, x) -> float:
super().__call__(x)
tot = np.sum(x)
if tot == 0:
return 0
calc = np.abs(tot)
calc *= 10000
calc **= 0.5
calc += 1
return calc
@property
def min_x(self) -> Sequence[float]:
return [7.70628098] * self.dims
@property
def min_fx(self) -> float:
return -self.dims
@property
def bounds(self) -> Sequence[Tuple[float, float]]:
return [[-10, 10]] * self.dims