# -*- coding: utf-8 -*- import numpy as np from numpy import abs, asarray class safe_import: def __enter__(self): self.error = False return self def __exit__(self, type_, value, traceback): if type_ is not None: self.error = True suppress = not ( os.getenv("SCIPY_ALLOW_BENCH_IMPORT_ERRORS", "1").lower() in ("0", "false") or not issubclass(type_, ImportError) ) return suppress with safe_import(): from scipy.special import factorial class Benchmark: """ Defines a global optimization benchmark problem. This abstract class defines the basic structure of a global optimization problem. Subclasses should implement the ``fun`` method for a particular optimization problem. Attributes ---------- N : int The dimensionality of the problem. bounds : sequence The lower/upper bounds to be used for minimizing the problem. This a list of (lower, upper) tuples that contain the lower and upper bounds for the problem. The problem should not be asked for evaluation outside these bounds. ``len(bounds) == N``. xmin : sequence The lower bounds for the problem xmax : sequence The upper bounds for the problem fglob : float The global minimum of the evaluated function. global_optimum : sequence A list of vectors that provide the locations of the global minimum. Note that some problems have multiple global minima, not all of which may be listed. nfev : int the number of function evaluations that the object has been asked to calculate. change_dimensionality : bool Whether we can change the benchmark function `x` variable length (i.e., the dimensionality of the problem) custom_bounds : sequence a list of tuples that contain lower/upper bounds for use in plotting. """ def __init__(self, dimensions): """ Initialises the problem Parameters ---------- dimensions : int The dimensionality of the problem """ self._dimensions = dimensions self.nfev = 0 self.fglob = np.nan self.global_optimum = None self.change_dimensionality = False self.custom_bounds = None def __str__(self): return '{0} ({1} dimensions)'.format(self.__class__.__name__, self.N) def __repr__(self): return self.__class__.__name__ def initial_vector(self): """ Random initialisation for the benchmark problem. Returns ------- x : sequence a vector of length ``N`` that contains random floating point numbers that lie between the lower and upper bounds for a given parameter. """ return asarray([np.random.uniform(l, u) for l, u in self.bounds]) def success(self, x, tol=1.e-5): """ Tests if a candidate solution at the global minimum. The default test is Parameters ---------- x : sequence The candidate vector for testing if the global minimum has been reached. Must have ``len(x) == self.N`` tol : float The evaluated function and known global minimum must differ by less than this amount to be at a global minimum. Returns ------- bool : is the candidate vector at the global minimum? """ val = self.fun(asarray(x)) if abs(val - self.fglob) < tol: return True # the solution should still be in bounds, otherwise immediate fail. if np.any(x > np.asfarray(self.bounds)[:, 1]): return False if np.any(x < np.asfarray(self.bounds)[:, 0]): return False # you found a lower global minimum. This shouldn't happen. if val < self.fglob: raise ValueError("Found a lower global minimum", x, val, self.fglob) return False def fun(self, x): """ Evaluation of the benchmark function. Parameters ---------- x : sequence The candidate vector for evaluating the benchmark problem. Must have ``len(x) == self.N``. Returns ------- val : float the evaluated benchmark function """ raise NotImplementedError def change_dimensions(self, ndim): """ Changes the dimensionality of the benchmark problem The dimensionality will only be changed if the problem is suitable Parameters ---------- ndim : int The new dimensionality for the problem. """ if self.change_dimensionality: self._dimensions = ndim else: raise ValueError('dimensionality cannot be changed for this' 'problem') @property def bounds(self): """ The lower/upper bounds to be used for minimizing the problem. This a list of (lower, upper) tuples that contain the lower and upper bounds for the problem. The problem should not be asked for evaluation outside these bounds. ``len(bounds) == N``. """ if self.change_dimensionality: return [self._bounds[0]] * self.N else: return self._bounds @property def N(self): """ The dimensionality of the problem. Returns ------- N : int The dimensionality of the problem """ return self._dimensions @property def xmin(self): """ The lower bounds for the problem Returns ------- xmin : sequence The lower bounds for the problem """ return asarray([b[0] for b in self.bounds]) @property def xmax(self): """ The upper bounds for the problem Returns ------- xmax : sequence The upper bounds for the problem """ return asarray([b[1] for b in self.bounds])