Source code for pymoo.algorithms.soo.nonconvex.pattern

"""Hooke and Jeeves pattern search local search algorithm."""

import numpy as np

from pymoo.algorithms.base.local import LocalSearch
from pymoo.core.individual import Individual
from pymoo.core.population import Population
from pymoo.core.replacement import is_better
from pymoo.docs import parse_doc_string
from pymoo.operators.repair.to_bound import set_to_bounds_if_outside_by_problem
from pymoo.util.display.single import SingleObjectiveOutput
from pymoo.util.optimum import filter_optimum
from pymoo.util import default_random_state


# =========================================================================================================
# Implementation
# =========================================================================================================


[docs] class PatternSearch(LocalSearch): def __init__( self, init_delta=0.25, init_rho=0.5, step_size=1.0, output=SingleObjectiveOutput(), **kwargs, ): """Hooke and Jeeves Pattern Search local optimizer. Args: init_delta: Initial step size for exploration. Scaled relative to bounds. init_rho: Contraction factor for step size reduction on unsuccessful moves. step_size: Scaling factor for pattern moves along promising directions. output: Output handler for convergence information. **kwargs: Additional arguments passed to parent class. """ super().__init__(output=output, **kwargs) self.init_rho = init_rho self.init_delta = init_delta self.step_size = step_size self.n_not_improved = 0 self._rho = init_rho self._delta = None self._center = None self._current = None self._trial = None self._direction = None self._sign = None def _initialize_advance(self, infills=None, **kwargs): super()._initialize_advance(infills=infills, **kwargs) self._center, self._explr = self.x0, self.x0 self._sign = np.ones(self.problem.n_var) if self.problem.has_bounds(): xl, xu = self.problem.bounds() self._delta = self.init_delta * (xu - xl) else: self._delta = np.abs(self.x0.X) / 2.0 self._delta[self._delta <= 1.0] = 1.0 def _next(self): # whether the last iteration has resulted in a new optimum or not has_improved = is_better(self._explr, self._center) # that means that the exploration did not find any new point and was thus unsuccessful if not has_improved: # increase the counter (by default this will be initialized to 0 and directly increased to 1) self.n_not_improved += 1 # keep track of the rho values in the normalized space self._rho = self.init_rho**self.n_not_improved # explore around the current center - try finding a suitable direction self._explr = yield from exploration_move( self.problem, self._center, self._sign, self._delta, self._rho ) # if we have found a direction in the last iteration to be worth following else: # get the direction which was successful in the last move self._direction = self._explr.X - self._center.X # declare the exploration point the new center (it has led to an improvement in the last iteration!) self._center = self._explr # use the pattern move to get a new trial vector along that given direction self._trial = yield pattern_move( self.problem, self._center, self._direction, self.step_size ) # get the delta sign adjusted for the exploration self._sign = calc_sign(self._direction) # explore around the current center to try finding a suitable direction self._explr = yield from exploration_move( self.problem, self._trial, self._sign, self._delta, self._rho ) self.pop = Population.create(self._center, self._explr) def _set_optimum(self): pop = self.pop if self.opt is None else Population.merge(self.opt, self.pop) self.opt = filter_optimum(pop, least_infeasible=True)
@default_random_state def exploration_move( problem, center, sign, delta, rho, randomize=True, random_state=None ): n_var = problem.n_var # the order for the variable iteration if randomize: K = random_state.permutation(n_var) else: K = np.arange(n_var) # iterate over each variable for k in K: # the value to be tried first is given by the amount times the sign _delta = sign[k] * rho * delta # make a step of delta on the k-th variable _explr = yield step_along_axis(problem, center.X, _delta, k) if is_better(_explr, center, eps=0.0): center = _explr # if not successful try the other direction else: # now try the negative value of delta and see if we can improve _explr = yield step_along_axis(problem, center.X, -1 * _delta, k) if is_better(_explr, center, eps=0.0): center = _explr return center def pattern_move(problem, current, direction, step_size): # calculate the new X and repair out of bounds if necessary X = current.X + step_size * direction set_to_bounds_if_outside_by_problem(problem, X) # create the new center individual return Individual(X=X) def calc_sign(direction): sign = np.sign(direction) sign[sign == 0] = -1 return sign def step_along_axis(problem, x, delta, axis): # copy and add delta to the new point X = np.copy(x) # now add to the current solution X[axis] = X[axis] + delta[axis] # repair if out of bounds if necessary X = set_to_bounds_if_outside_by_problem(problem, X) return Individual(X=X) parse_doc_string(PatternSearch.__init__)