Skip to content

Commit 6e85f90

Browse files
author
Julian Blank
committed
PatternSearch Correct Implementation
1 parent 7397198 commit 6e85f90

File tree

8 files changed

+926
-68
lines changed

8 files changed

+926
-68
lines changed

pymoo/algorithms/so_pattern_search.py

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,25 @@
1818

1919
class PatternSearchDisplay(SingleObjectiveDisplay):
2020

21+
def __init__(self, **kwargs):
22+
super().__init__(favg=False, **kwargs)
23+
2124
def _do(self, problem, evaluator, algorithm):
2225
super()._do(problem, evaluator, algorithm)
23-
self.output.append("T", algorithm.T)
26+
self.output.append("delta", np.max(np.abs(algorithm.explr_delta)))
2427

2528

2629
class PatternSearchTermination(Termination):
2730

28-
def __init__(self, min_T=1e-5, **kwargs):
31+
def __init__(self, eps=1e-5, **kwargs):
2932
super().__init__()
3033
self.default = SingleObjectiveDefaultTermination(**kwargs)
31-
self.min_T = min_T
34+
self.eps = eps
3235

3336
def do_continue(self, algorithm):
3437
decision_default = self.default.do_continue(algorithm)
35-
if algorithm.T < self.min_T:
38+
delta = np.max(np.abs(algorithm.explr_delta))
39+
if delta < self.eps:
3640
return decision_default
3741
else:
3842
return True
@@ -41,61 +45,89 @@ def do_continue(self, algorithm):
4145
class PatternSearch(LocalSearch):
4246

4347
def __init__(self,
44-
T=0.1,
45-
a=2,
48+
explr_delta=0.25,
49+
explr_rho=0.5,
50+
pattern_step=2,
51+
eps=1e-5,
4652
display=PatternSearchDisplay(),
4753
**kwargs):
54+
4855
super().__init__(display=display, **kwargs)
49-
self.T = T
50-
self.a = a
51-
self.default_termination = PatternSearchTermination(x_tol=1e-6, f_tol=1e-6, nth_gen=1, n_last=2)
56+
self.explr_rho = explr_rho
57+
self.pattern_step = pattern_step
58+
self.explr_delta = explr_delta
59+
self.default_termination = PatternSearchTermination(eps=eps, x_tol=1e-6, f_tol=1e-6, nth_gen=1, n_last=2)
5260

53-
def _next(self):
54-
# the current best solution found so far
55-
best = self.opt[0]
61+
def _initialize(self, **kwargs):
62+
super()._initialize(**kwargs)
5663

57-
# first do the exploration move
58-
opt = self._exploration_move(best)
64+
# make delta a vector - the sign is later updated individually
65+
if not isinstance(self.explr_delta, np.ndarray):
66+
self.explr_delta = np.ones(self.problem.n_var) * self.explr_delta
5967

60-
# if the exploration move could not improve the current solution
61-
if opt == best:
62-
self.T = self.T / 2
68+
def _next(self):
6369

64-
# if the move brought up a new solution -> perform a line search
65-
else:
66-
self._pattern_move(best, opt)
70+
# in the beginning of each iteration first do an exploration move
71+
self._previous = self.opt[0]
72+
self._current = self._exploration_move(self._previous)
6773

68-
def _pattern_move(self, old_best, new_best):
69-
_current, _next = old_best, new_best
74+
# one iteration is the combination of this two moves repeatedly until delta needs to be reduced
75+
while self._previous != self._current:
7076

71-
while True:
72-
X = _current.X + self.a * (_next.X - _current.X)
73-
xl, xu = self.problem.bounds()
74-
X = repair_out_of_bounds_manually(X, xl, xu)
75-
tentative = Individual(X=X)
77+
# use the pattern move to get a new trial vector
78+
trial = self._pattern_move(self._previous, self._current)
7679

77-
self.evaluator.eval(self.problem, tentative, algorithm=self)
78-
self.pop = Population.merge(self.pop, tentative)
80+
# perform an exploration move around the trial vector - the best known solution is always stored in _current
81+
explr = self._exploration_move(trial, opt=self._current)
7982

80-
# if the tentative could not further improve the old best
81-
if not is_better(tentative, _next):
83+
if not is_better(explr, self._current):
8284
break
83-
else:
84-
# if we have improved
85-
_current, _next = _next, tentative
8685

87-
def _exploration_move(self, opt):
88-
xl, xu = self.problem.bounds()
86+
self._previous, self._current = self._current, explr
87+
88+
self.explr_delta *= self.explr_rho
89+
90+
def _pattern_move(self, _current, _next):
91+
92+
# get the direction and assign the corresponding delta value
93+
direction = (_next.X - _current.X)
94+
95+
# get the delta sign adjusted
96+
sign = np.sign(direction)
97+
sign[sign == 0] = -1
98+
self.explr_delta = sign * np.abs(self.explr_delta)
8999

90-
def step(x, sign):
91-
# copy to not modify the original value
100+
# calculate the new X and repair out of bounds if necessary
101+
X = _current.X + self.pattern_step * direction
102+
repair_out_of_bounds_manually(X, *self.problem.bounds())
103+
104+
# create the new center individual without evaluating it
105+
trial = Individual(X=X)
106+
107+
return trial
108+
109+
def _exploration_move(self, center, opt=None):
110+
if opt is None:
111+
opt = center
112+
113+
def step(x, delta, k):
114+
115+
# copy and add delta to the new point
92116
X = np.copy(x)
93117

94-
# add the value in the normalized space to the k-th component
95-
X[k] = X[k] + (sign * self.T) * (xu[k] - xl[k])
118+
# normalize the delta by the bounds if they are provided by the problem
119+
eps = delta[k]
96120

97-
# repair if out of bounds
98-
X = repair_out_of_bounds_manually(X, xl, xu)
121+
# if the problem has bounds normalize the delta
122+
if self.problem.has_bounds():
123+
xl, xu = self.problem.bounds()
124+
eps *= (xu[k] - xl[k])
125+
126+
# now add to the current solution
127+
X[k] = X[k] + eps
128+
129+
# repair if out of bounds if necessary
130+
X = repair_out_of_bounds_manually(X, *self.problem.bounds())
99131

100132
# return the new solution as individual
101133
mutant = pop_from_array_or_individual(X)[0]
@@ -104,26 +136,26 @@ def step(x, sign):
104136

105137
for k in range(self.problem.n_var):
106138

107-
# randomly assign + or - as a sign
108-
sign = 1 if np.random.random() < 0.5 else -1
109-
110139
# create the the individual and evaluate it
111-
mutant = step(opt.X, sign)
140+
mutant = step(center.X, self.explr_delta, k)
112141
self.evaluator.eval(self.problem, mutant, algorithm=self)
113142
self.pop = Population.merge(self.pop, mutant)
114143

115144
if is_better(mutant, opt):
116-
opt = mutant
145+
center, opt = mutant, mutant
117146

118147
else:
119148

149+
# inverse the sign of the delta
150+
self.explr_delta[k] = - self.explr_delta[k]
151+
120152
# now try the other sign if there was no improvement
121-
mutant = step(opt.X, -1 * sign)
153+
mutant = step(center.X, self.explr_delta, k)
122154
self.evaluator.eval(self.problem, mutant, algorithm=self)
123155
self.pop = Population.merge(self.pop, mutant)
124156

125157
if is_better(mutant, opt):
126-
opt = mutant
158+
center, opt = mutant, mutant
127159

128160
return opt
129161

pymoo/problems/single/rosenbrock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ def __init__(self, n_var=2):
1010
def _evaluate(self, x, out, *args, **kwargs):
1111
l = []
1212
for i in range(x.shape[1] - 1):
13-
l.append(100 * anp.square((x[:, i + 1] - anp.square(x[:, i]))) + anp.square((1 - x[:, i])))
13+
val = 100 * (x[:, i + 1] - x[:, i] ** 2) ** 2 + (1 - x[:, i]) ** 2
14+
l.append(val)
1415
out["F"] = anp.sum(anp.column_stack(l), axis=1)
1516

1617
def _calc_pareto_front(self):

pymoo/usage/algorithms/usage_pattern_search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pymoo.factory import get_problem
33
from pymoo.optimize import minimize
44

5-
problem = get_problem("ackley")
5+
problem = get_problem("ackley", n_var=30)
66

77
algorithm = PatternSearch()
88

pymoo/util/display.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ def _do(self, problem, evaluator, algorithm):
118118

119119
class SingleObjectiveDisplay(Display):
120120

121+
def __init__(self, favg=True, **kwargs):
122+
super().__init__(**kwargs)
123+
self.favg = favg
124+
121125
def _do(self, problem, evaluator, algorithm):
122126
super()._do(problem, evaluator, algorithm)
123127

@@ -132,9 +136,12 @@ def _do(self, problem, evaluator, algorithm):
132136
if len(feasible) > 0:
133137
_F = F[feasible]
134138
self.output.append("fopt", opt.F[0])
135-
self.output.append("favg", np.mean(_F))
139+
if self.favg:
140+
self.output.append("favg", np.mean(_F))
136141
else:
137-
self.output.extend(*[('fopt', "-"), ('favg', "-")])
142+
self.output.append("fopt", "-")
143+
if self.favg:
144+
self.output.append("favg", "-")
138145

139146

140147
class MultiObjectiveDisplay(Display):
@@ -180,12 +187,3 @@ def _do(self, problem, evaluator, algorithm):
180187
self.output.append("delta_ideal", delta_ideal)
181188
self.output.append("delta_nadir", delta_nadir)
182189
self.output.append("delta_f", delta_f)
183-
184-
185-
186-
187-
188-
189-
190-
191-

pymoo/util/reference_direction.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy as np
44
from scipy import special
55

6-
from pymoo.util.misc import find_duplicates, vectorized_cdist, cdist
6+
from pymoo.util.misc import find_duplicates, cdist
77

88

99
# =========================================================================================================
@@ -21,7 +21,7 @@ def __init__(self, n_dim, scaling=None, lexsort=True, verbose=False, seed=None,
2121
self.verbose = verbose
2222
self.seed = seed
2323

24-
def do(self, seed=None):
24+
def do(self):
2525

2626
# set the random seed if it is provided
2727
if self.seed is not None:

tests/algorithms/pattern_search/__init__.py

Whitespace-only changes.

tests/algorithms/test_nelder_mead.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ def test_no_bounds(self):
1414
problem = get_problem("rastrigin")
1515
problem.xl = None
1616
problem.xu = None
17-
method = NelderMead(X=np.array([1, 1]), max_restarts=0)
17+
method = NelderMead(x0=np.array([1, 1]), max_restarts=0)
1818
minimize(problem, method, verbose=False)
1919

2020
def test_with_bounds_no_restart(self):
2121
problem = get_problem("rastrigin")
22-
method = NelderMead(X=np.array([1, 1]), max_restarts=0)
22+
method = NelderMead(x0=np.array([1, 1]), max_restarts=0)
2323
minimize(problem, method, verbose=False)
2424

2525
def test_with_bounds_no_initial_point(self):
@@ -29,7 +29,7 @@ def test_with_bounds_no_initial_point(self):
2929

3030
def test_with_bounds_with_restart(self):
3131
problem = get_problem("rastrigin")
32-
method = NelderMead(X=np.array([1, 1]), max_restarts=2)
32+
method = NelderMead(x0=np.array([1, 1]), max_restarts=2)
3333
minimize(problem, method, verbose=False)
3434

3535
def test_against_scipy(self):
@@ -56,7 +56,7 @@ def callback(x):
5656
hist.append(x)
5757

5858
problem.callback = callback
59-
minimize(problem, NelderMead(X=x0, max_restarts=0, termination=get_termination("n_eval", len(hist_scipy))))
59+
minimize(problem, NelderMead(x0=x0, max_restarts=0, termination=get_termination("n_eval", len(hist_scipy))))
6060
hist = np.row_stack(hist)[:len(hist_scipy)]
6161

6262
self.assertTrue(np.all(hist - hist_scipy < 1e-7))

0 commit comments

Comments
 (0)