fcmaes 1.1.3__py3-none-any.whl → 1.6.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fcmaes/__init__.py +12 -2
- fcmaes/advretry.py +217 -159
- fcmaes/astro.py +143 -27
- fcmaes/bitecpp.py +107 -0
- fcmaes/cmaes.py +204 -173
- fcmaes/cmaescpp.py +253 -87
- fcmaes/crfmnes.py +339 -0
- fcmaes/crfmnescpp.py +273 -0
- fcmaes/dacpp.py +39 -51
- fcmaes/de.py +472 -0
- fcmaes/decpp.py +222 -64
- fcmaes/diversifier.py +357 -0
- fcmaes/evaluator.py +297 -14
- fcmaes/lib/libacmalib.dll +0 -0
- fcmaes/lib/libacmalib.dylib +0 -0
- fcmaes/lib/libacmalib.so +0 -0
- fcmaes/lib/libhbv.so +0 -0
- fcmaes/lib/liblrgv.so +0 -0
- fcmaes/lib/librw_top_trumps.dll +0 -0
- fcmaes/lib/librw_top_trumps.so +0 -0
- fcmaes/mapelites.py +737 -0
- fcmaes/mode.py +719 -0
- fcmaes/modecpp.py +470 -0
- fcmaes/moretry.py +270 -0
- fcmaes/multiretry.py +195 -0
- fcmaes/optimizer.py +883 -112
- fcmaes/pgpecpp.py +340 -0
- fcmaes/pygmoretry.py +10 -19
- fcmaes/retry.py +248 -121
- fcmaes/test_cma.py +207 -30
- fcmaes/testfun.py +38 -1
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/METADATA +22 -12
- fcmaes-1.6.9.dist-info/RECORD +36 -0
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/WHEEL +1 -1
- fcmaes/hhcpp.py +0 -114
- fcmaes/lib/libgtoplib.dll +0 -0
- fcmaes/lib/libgtoplib.so +0 -0
- fcmaes-1.1.3.dist-info/RECORD +0 -23
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/LICENSE +0 -0
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/top_level.txt +0 -0
fcmaes/mode.py
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
# Copyright (c) Dietmar Wolz.
|
|
2
|
+
#
|
|
3
|
+
# This source code is licensed under the MIT license found in the
|
|
4
|
+
# LICENSE file in the root directory.
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
""" Numpy based implementation of multi objective
|
|
8
|
+
Differential Evolution using either the DE/rand/1 strategy
|
|
9
|
+
or a NSGA-II like population update (parameter 'nsga_update=True)'.
|
|
10
|
+
Then it works similar to NSGA-II.
|
|
11
|
+
|
|
12
|
+
Supports parallel fitness function evaluation.
|
|
13
|
+
|
|
14
|
+
Features enhanced multiple constraint ranking (https://www.jstage.jst.go.jp/article/tjpnsec/11/2/11_18/_article/-char/en/)
|
|
15
|
+
improving its performance in handling constraints for engineering design optimization.
|
|
16
|
+
|
|
17
|
+
Enables the comparison of DE and NSGA-II population update mechanism with everything else
|
|
18
|
+
kept completely identical.
|
|
19
|
+
|
|
20
|
+
Requires python 3.5 or higher.
|
|
21
|
+
|
|
22
|
+
Uses the following deviation from the standard DE algorithm:
|
|
23
|
+
a) oscillating CR/F parameters.
|
|
24
|
+
|
|
25
|
+
You may keep parameters F and CR at their defaults since this implementation works well with the given settings for most problems,
|
|
26
|
+
since the algorithm oscillates between different F and CR settings.
|
|
27
|
+
|
|
28
|
+
For expensive objective functions (e.g. machine learning parameter optimization) use the workers
|
|
29
|
+
parameter to parallelize objective function evaluation. The workers parameter is limited by the
|
|
30
|
+
population size.
|
|
31
|
+
|
|
32
|
+
The ints parameter is a boolean array indicating which parameters are discrete integer values. This
|
|
33
|
+
parameter was introduced after observing non optimal DE-results for the ESP2 benchmark problem:
|
|
34
|
+
https://github.com/AlgTUDelft/ExpensiveOptimBenchmark/blob/master/expensiveoptimbenchmark/problems/DockerCFDBenchmark.py
|
|
35
|
+
If defined it causes a "special treatment" for discrete variables: They are rounded to the next integer value and
|
|
36
|
+
there is an additional mutation to avoid getting stuck at local minima. This behavior is specified by the internal
|
|
37
|
+
function _modifier which can be overwritten by providing the optional modifier argument. If modifier is defined,
|
|
38
|
+
ints is ignored.
|
|
39
|
+
|
|
40
|
+
See https://github.com/dietmarwo/fast-cma-es/blob/master/tutorials/MODE.adoc for a detailed description.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
import os, sys, time
|
|
45
|
+
import ctypes as ct
|
|
46
|
+
from numpy.random import Generator, PCG64DXSM
|
|
47
|
+
from scipy.optimize import Bounds
|
|
48
|
+
|
|
49
|
+
from fcmaes.evaluator import Evaluator, parallel_mo
|
|
50
|
+
from fcmaes import moretry
|
|
51
|
+
import multiprocessing as mp
|
|
52
|
+
from fcmaes.optimizer import dtime
|
|
53
|
+
from fcmaes.retry import Shared2d
|
|
54
|
+
from loguru import logger
|
|
55
|
+
from typing import Optional, Callable, Tuple
|
|
56
|
+
from numpy.typing import ArrayLike
|
|
57
|
+
|
|
58
|
+
os.environ['MKL_DEBUG_CPU_TYPE'] = '5'
|
|
59
|
+
|
|
60
|
+
def minimize(mofun: Callable[[ArrayLike], ArrayLike],
|
|
61
|
+
nobj: int,
|
|
62
|
+
ncon: int,
|
|
63
|
+
bounds: Bounds,
|
|
64
|
+
guess: Optional[np.ndarray] = None,
|
|
65
|
+
popsize: Optional[int] = 64,
|
|
66
|
+
max_evaluations: Optional[int] = 100000,
|
|
67
|
+
workers: Optional[int] = 1,
|
|
68
|
+
f: Optional[float] = 0.5,
|
|
69
|
+
cr: Optional[float] = 0.9,
|
|
70
|
+
pro_c: Optional[float] = 0.5,
|
|
71
|
+
dis_c: Optional[float] = 15.0,
|
|
72
|
+
pro_m: Optional[float] = 0.9,
|
|
73
|
+
dis_m: Optional[float] = 20.0,
|
|
74
|
+
nsga_update: Optional[bool] = True,
|
|
75
|
+
pareto_update: Optional[int] = 0,
|
|
76
|
+
ints: Optional[ArrayLike] = None,
|
|
77
|
+
modifier: Callable = None,
|
|
78
|
+
min_mutate: Optional[float] = 0.1,
|
|
79
|
+
max_mutate: Optional[float] = 0.5,
|
|
80
|
+
rg: Optional[Generator] = Generator(PCG64DXSM()),
|
|
81
|
+
store: Optional[store] = None) -> Tuple[np.ndarray, np.ndarray]:
|
|
82
|
+
|
|
83
|
+
"""Minimization of a multi objjective function of one or more variables using
|
|
84
|
+
Differential Evolution.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
mofun : callable
|
|
89
|
+
The objective function to be minimized.
|
|
90
|
+
``mofun(x) -> ndarray(float)``
|
|
91
|
+
where ``x`` is an 1-D array with shape (n,)
|
|
92
|
+
nobj : int
|
|
93
|
+
number of objectives
|
|
94
|
+
ncon : int
|
|
95
|
+
number of constraints, default is 0.
|
|
96
|
+
The objective function needs to return vectors of size nobj + ncon
|
|
97
|
+
bounds : sequence or `Bounds`
|
|
98
|
+
Bounds on variables. There are two ways to specify the bounds:
|
|
99
|
+
1. Instance of the `scipy.Bounds` class.
|
|
100
|
+
2. Sequence of ``(min, max)`` pairs for each element in `x`. None
|
|
101
|
+
is used to specify no bound.
|
|
102
|
+
guess : ndarray, shape (popsize,dim) or Tuple
|
|
103
|
+
Initial guess.
|
|
104
|
+
popsize : int, optional
|
|
105
|
+
Population size.
|
|
106
|
+
max_evaluations : int, optional
|
|
107
|
+
Forced termination after ``max_evaluations`` function evaluations.
|
|
108
|
+
workers : int or None, optional
|
|
109
|
+
workers > 1, function evaluation is performed in parallel for the whole population.
|
|
110
|
+
Useful for costly objective functions
|
|
111
|
+
f = float, optional
|
|
112
|
+
The mutation constant. In the literature this is also known as differential weight,
|
|
113
|
+
being denoted by F. Should be in the range [0, 2].
|
|
114
|
+
cr = float, optional
|
|
115
|
+
The recombination constant. Should be in the range [0, 1].
|
|
116
|
+
In the literature this is also known as the crossover probability.
|
|
117
|
+
pro_c, dis_c, pro_m, dis_m = float, optional
|
|
118
|
+
NSGA population update parameters, usually leave at default.
|
|
119
|
+
nsga_update = boolean, optional
|
|
120
|
+
Use of NSGA-II/SBX or DE population update. Default is True
|
|
121
|
+
pareto_update = float, optional
|
|
122
|
+
Only applied if nsga_update = False. Favor better solutions for sample generation. Default 0 -
|
|
123
|
+
use all population members with the same probability.
|
|
124
|
+
ints = list or array, optional
|
|
125
|
+
indicating which parameters are discrete integer values. If defined these parameters will be
|
|
126
|
+
rounded to the next integer and some additional mutation of discrete parameters are performed.
|
|
127
|
+
min_mutate = float, optional
|
|
128
|
+
Determines the minimal mutation rate for discrete integer parameters.
|
|
129
|
+
max_mutate = float, optional
|
|
130
|
+
Determines the maximal mutation rate for discrete integer parameters.
|
|
131
|
+
modifier = callable, optional
|
|
132
|
+
used to overwrite the default behaviour induced by ints. If defined, the ints parameter is
|
|
133
|
+
ignored. Modifies all generated x vectors.
|
|
134
|
+
rg = numpy.random.Generator, optional
|
|
135
|
+
Random generator for creating random guesses.
|
|
136
|
+
store : result store, optional
|
|
137
|
+
if defined the optimization results are added to the result store. For multi threaded execution.
|
|
138
|
+
use workers=1 if you call minimize from multiple threads
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
x, y: list of argument vectors and corresponding value vectors of the optimization results. """
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
mode = MODE(nobj, ncon, bounds, popsize,
|
|
146
|
+
f, cr, pro_c, dis_c, pro_m, dis_m, nsga_update, pareto_update, rg, ints, min_mutate, max_mutate, modifier)
|
|
147
|
+
mode.set_guess(guess, mofun, rg)
|
|
148
|
+
if workers <= 1:
|
|
149
|
+
x, y, = mode.minimize_ser(mofun, max_evaluations)
|
|
150
|
+
else:
|
|
151
|
+
x, y = mode.minimize_par(mofun, max_evaluations, workers)
|
|
152
|
+
if not store is None:
|
|
153
|
+
store.add_results(x, y)
|
|
154
|
+
return x, y
|
|
155
|
+
except Exception as ex:
|
|
156
|
+
print(str(ex))
|
|
157
|
+
return None, None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class store():
|
|
161
|
+
|
|
162
|
+
"""Result store. Used for multi threaded execution of minimize to collect optimization results.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
dim : int
|
|
167
|
+
dimension - number of variables
|
|
168
|
+
nobj : int
|
|
169
|
+
number of objectives
|
|
170
|
+
capacity : int, optional
|
|
171
|
+
capacity of the store collecting all solutions. If full, its content is replaced by its
|
|
172
|
+
pareto front.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, dim, nobj, capacity = mp.cpu_count()*512):
|
|
176
|
+
self.dim = dim
|
|
177
|
+
self.nobj = nobj
|
|
178
|
+
self.capacity = capacity
|
|
179
|
+
self.add_mutex = mp.Lock()
|
|
180
|
+
self.xs = Shared2d(np.empty((self.capacity, self.dim), dtype = np.float64))
|
|
181
|
+
self.ys = Shared2d(np.empty((self.capacity, self.nobj), dtype = np.float64))
|
|
182
|
+
self.create_views()
|
|
183
|
+
self.num_stored = mp.RawValue(ct.c_int, 0)
|
|
184
|
+
self.num_added = mp.RawValue(ct.c_int, 0)
|
|
185
|
+
|
|
186
|
+
def create_views(self): # needs to be called in the target process
|
|
187
|
+
self.xs_view = self.xs.view()
|
|
188
|
+
self.ys_view = self.ys.view()
|
|
189
|
+
|
|
190
|
+
def get_xs(self) -> np.ndarray:
|
|
191
|
+
return self.xs.view()
|
|
192
|
+
|
|
193
|
+
def get_ys(self) -> np.ndarray:
|
|
194
|
+
return self.ys.view()
|
|
195
|
+
|
|
196
|
+
def add_result(self, x, y):
|
|
197
|
+
with self.add_mutex:
|
|
198
|
+
self.num_added.value += 1
|
|
199
|
+
i = self.num_stored.value
|
|
200
|
+
if i < self.capacity:
|
|
201
|
+
self.xs_view[i] = x
|
|
202
|
+
self.ys_view[i] = y
|
|
203
|
+
self.num_stored.value = i + 1
|
|
204
|
+
|
|
205
|
+
def add_results(self, xs, ys):
|
|
206
|
+
with self.add_mutex:
|
|
207
|
+
self.num_added.value += 1
|
|
208
|
+
i = self.num_stored.value
|
|
209
|
+
for j in range(len(xs)):
|
|
210
|
+
if i < self.capacity:
|
|
211
|
+
self.xs_view[i] = xs[j]
|
|
212
|
+
self.ys_view[i] = ys[j][:self.nobj]
|
|
213
|
+
i += 1
|
|
214
|
+
else:
|
|
215
|
+
self.get_front(update=True)
|
|
216
|
+
i = self.num_stored.value
|
|
217
|
+
if i > 0.9*self.capacity: # give up
|
|
218
|
+
return
|
|
219
|
+
self.num_stored.value = i
|
|
220
|
+
|
|
221
|
+
def get_front(self, update=False):
|
|
222
|
+
stored = self.num_stored.value
|
|
223
|
+
xs = self.xs_view[:stored]
|
|
224
|
+
ys = self.ys_view[:stored]
|
|
225
|
+
xf, yf = moretry.pareto(xs, ys)
|
|
226
|
+
if update:
|
|
227
|
+
n = len(yf)
|
|
228
|
+
self.xs_view[:n] = xf
|
|
229
|
+
self.ys_view[:n] = yf
|
|
230
|
+
self.num_stored.value = n
|
|
231
|
+
return xf, yf
|
|
232
|
+
|
|
233
|
+
def get_content(self):
|
|
234
|
+
stored = self.num_stored.value
|
|
235
|
+
return self.xs_view[:stored], self.ys_view[:stored]
|
|
236
|
+
|
|
237
|
+
class MODE(object):
|
|
238
|
+
|
|
239
|
+
def __init__(self,
|
|
240
|
+
nobj: int,
|
|
241
|
+
ncon: int,
|
|
242
|
+
bounds: Bounds,
|
|
243
|
+
popsize: Optional[int] = 64,
|
|
244
|
+
F: Optional[float] = 0.5,
|
|
245
|
+
Cr: Optional[float] = 0.9,
|
|
246
|
+
pro_c: Optional[float] = 0.5,
|
|
247
|
+
dis_c: Optional[float] = 15.0,
|
|
248
|
+
pro_m: Optional[float] = 0.9,
|
|
249
|
+
dis_m: Optional[float] = 20.0,
|
|
250
|
+
nsga_update: Optional[bool] = True,
|
|
251
|
+
pareto_update: Optional[int] = 0,
|
|
252
|
+
rg: Optional[Generator] = Generator(PCG64DXSM()),
|
|
253
|
+
ints: Optional[ArrayLike] = None,
|
|
254
|
+
min_mutate: Optional[float] = 0.1,
|
|
255
|
+
max_mutate: Optional[float] = 0.5,
|
|
256
|
+
modifier: Callable = None):
|
|
257
|
+
self.nobj = nobj
|
|
258
|
+
self.ncon = ncon
|
|
259
|
+
self.dim, self.lower, self.upper = _check_bounds(bounds, None)
|
|
260
|
+
if popsize is None:
|
|
261
|
+
popsize = 64
|
|
262
|
+
if popsize % 2 == 1 and nsga_update: # nsga update requires even popsize
|
|
263
|
+
popsize += 1
|
|
264
|
+
self.popsize = popsize
|
|
265
|
+
self.rg = rg
|
|
266
|
+
self.F0 = F
|
|
267
|
+
self.Cr0 = Cr
|
|
268
|
+
self.pro_c = pro_c
|
|
269
|
+
self.dis_c = dis_c
|
|
270
|
+
self.pro_m = pro_m
|
|
271
|
+
self.dis_m = dis_m
|
|
272
|
+
self.nsga_update = nsga_update
|
|
273
|
+
self.pareto_update = pareto_update
|
|
274
|
+
self.stop = 0
|
|
275
|
+
self.iterations = 0
|
|
276
|
+
self.evals = 0
|
|
277
|
+
self.mutex = mp.Lock()
|
|
278
|
+
self.p = 0
|
|
279
|
+
# nsga update doesn't support mixed integer
|
|
280
|
+
self.ints = None if (ints is None or nsga_update) else np.array(ints)
|
|
281
|
+
self.min_mutate = min_mutate
|
|
282
|
+
self.max_mutate = max_mutate
|
|
283
|
+
# use default variable modifier for int variables if modifier is None
|
|
284
|
+
if modifier is None and not ints is None:
|
|
285
|
+
self.lower = self.lower.astype(float)
|
|
286
|
+
self.upper = self.upper.astype(float)
|
|
287
|
+
self.modifier = self._modifier
|
|
288
|
+
else:
|
|
289
|
+
self.modifier = modifier
|
|
290
|
+
self._init()
|
|
291
|
+
|
|
292
|
+
def set_guess(self, guess, mofun, rg = None):
|
|
293
|
+
if not guess is None:
|
|
294
|
+
if isinstance(guess, np.ndarray):
|
|
295
|
+
ys = np.array([mofun(x) for x in guess])
|
|
296
|
+
else:
|
|
297
|
+
guess, ys = guess
|
|
298
|
+
if rg is None:
|
|
299
|
+
rg = Generator(PCG64DXSM())
|
|
300
|
+
choice = rg.choice(len(ys), self.popsize,
|
|
301
|
+
replace = (len(ys) < self.popsize))
|
|
302
|
+
self.tell(ys[choice], guess[choice])
|
|
303
|
+
|
|
304
|
+
def ask(self) -> np.ndarray:
|
|
305
|
+
for p in range(self.popsize):
|
|
306
|
+
self.x[p + self.popsize] = self._next_x(p)
|
|
307
|
+
return self.x[self.popsize:]
|
|
308
|
+
|
|
309
|
+
def tell(self, ys: np.ndarray, xs: Optional[np.ndarray] = None):
|
|
310
|
+
if not xs is None:
|
|
311
|
+
for p in range(self.popsize):
|
|
312
|
+
self.x[p + self.popsize] = xs[p]
|
|
313
|
+
for p in range(self.popsize):
|
|
314
|
+
self.y[p + self.popsize] = ys[p]
|
|
315
|
+
self.pop_update()
|
|
316
|
+
|
|
317
|
+
def _init(self):
|
|
318
|
+
self.x = np.empty((2*self.popsize, self.dim))
|
|
319
|
+
self.y = np.empty((2*self.popsize, self.nobj + self.ncon))
|
|
320
|
+
for i in range(self.popsize):
|
|
321
|
+
self.x[i] = self._sample()
|
|
322
|
+
self.y[i] = np.array([1E99]*(self.nobj + self.ncon))
|
|
323
|
+
self.vx = self.x.copy()
|
|
324
|
+
self.vp = 0
|
|
325
|
+
self.ycon = None
|
|
326
|
+
self.eps = 0
|
|
327
|
+
|
|
328
|
+
def minimize_ser(self,
|
|
329
|
+
fun: Callable[[ArrayLike], ArrayLike],
|
|
330
|
+
max_evaluations: Optional[int] = 100000) -> Tuple[np.ndarray, np.ndarray]:
|
|
331
|
+
evals = 0
|
|
332
|
+
while evals < max_evaluations:
|
|
333
|
+
xs = self.ask()
|
|
334
|
+
ys = np.array([fun(x) for x in xs])
|
|
335
|
+
self.tell(ys)
|
|
336
|
+
evals += self.popsize
|
|
337
|
+
return xs, ys
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def minimize_par(self,
|
|
341
|
+
fun: Callable[[ArrayLike], ArrayLike],
|
|
342
|
+
max_evaluations: Optional[int] = 100000,
|
|
343
|
+
workers: Optional[int] = mp.cpu_count()) -> Tuple[np.ndarray, np.ndarray]:
|
|
344
|
+
fit = parallel_mo(fun, self.nobj + self.ncon, workers)
|
|
345
|
+
evals = 0
|
|
346
|
+
while evals < max_evaluations:
|
|
347
|
+
xs = self.ask()
|
|
348
|
+
ys = fit(xs)
|
|
349
|
+
self.tell(ys)
|
|
350
|
+
evals += self.popsize
|
|
351
|
+
fit.stop()
|
|
352
|
+
return xs, ys
|
|
353
|
+
|
|
354
|
+
def pop_update(self):
|
|
355
|
+
y0 = self.y
|
|
356
|
+
x0 = self.x
|
|
357
|
+
if self.nobj == 1:
|
|
358
|
+
yi = np.flip(np.argsort(self.y[:,0]))
|
|
359
|
+
y0 = self.y[yi]
|
|
360
|
+
x0 = self.x[yi]
|
|
361
|
+
domination, self.ycon, self.eps = pareto_domination(y0, self.nobj, self.ncon, self.ycon, self.eps)
|
|
362
|
+
x = []
|
|
363
|
+
y = []
|
|
364
|
+
maxdom = int(max(domination))
|
|
365
|
+
for dom in range(maxdom, -1, -1):
|
|
366
|
+
domlevel = [p for p in range(len(domination)) if domination[p] == dom]
|
|
367
|
+
if len(domlevel) == 0:
|
|
368
|
+
continue
|
|
369
|
+
if len(x) + len(domlevel) <= self.popsize:
|
|
370
|
+
# whole level fits
|
|
371
|
+
x = [*x, *x0[domlevel]]
|
|
372
|
+
y = [*y, *y0[domlevel]]
|
|
373
|
+
else: # sort for crowding
|
|
374
|
+
nx = x0[domlevel]
|
|
375
|
+
ny = y0[domlevel]
|
|
376
|
+
si = [0]
|
|
377
|
+
if len(ny) > 1:
|
|
378
|
+
cd = crowd_dist(ny)
|
|
379
|
+
si = np.flip(np.argsort(cd))
|
|
380
|
+
for p in si:
|
|
381
|
+
if len(x) >= self.popsize:
|
|
382
|
+
break
|
|
383
|
+
x.append(nx[p])
|
|
384
|
+
y.append(ny[p])
|
|
385
|
+
break # we have filled popsize members
|
|
386
|
+
self.x[:self.popsize] = x[:self.popsize]
|
|
387
|
+
self.y[:self.popsize] = y[:self.popsize]
|
|
388
|
+
if self.nsga_update:
|
|
389
|
+
self.vx = variation(self.x[:self.popsize], self.lower, self.upper, self.rg,
|
|
390
|
+
pro_c = self.pro_c, dis_c = self.dis_c, pro_m = self.pro_m, dis_m = self.dis_m)
|
|
391
|
+
|
|
392
|
+
def _next_x(self, p):
|
|
393
|
+
if self.nsga_update: # use NSGA-II update strategy.
|
|
394
|
+
x = self.vx[self.vp]
|
|
395
|
+
self.vp = (self.vp + 1) % self.popsize # only use the elite
|
|
396
|
+
return x
|
|
397
|
+
# use standard DE/pareto/1 strategy.
|
|
398
|
+
if p == 0: # switch FR / CR every generation
|
|
399
|
+
self.iterations += 1
|
|
400
|
+
self.Cr = 0.5*self.Cr0 if self.iterations % 2 == 0 else self.Cr0
|
|
401
|
+
self.F = 0.5*self.F0 if self.iterations % 2 == 0 else self.F0
|
|
402
|
+
while True:
|
|
403
|
+
if self.pareto_update > 0: # sample elite solutions
|
|
404
|
+
r1, r2 = self.rg.integers(0, self.popsize, 2)
|
|
405
|
+
rb = int(self.popsize * (self.rg.random() ** (1.0 + self.pareto_update)))
|
|
406
|
+
else:
|
|
407
|
+
# sample from whole population
|
|
408
|
+
r1, r2, rb = self.rg.integers(0, self.popsize, 3)
|
|
409
|
+
if r1 != p and r1 != rb and r1 != r2 and r2 != rb \
|
|
410
|
+
and r2 != p and rb != p:
|
|
411
|
+
break
|
|
412
|
+
xp = self.x[p]
|
|
413
|
+
xb = self.x[rb]
|
|
414
|
+
x1 = self.x[r1]
|
|
415
|
+
x2 = self.x[r2]
|
|
416
|
+
x = self._feasible(xb + self.F * (x1 - x2))
|
|
417
|
+
r = self.rg.integers(0, self.dim)
|
|
418
|
+
tr = np.array(
|
|
419
|
+
[i != r and self.rg.random() > self.Cr for i in range(self.dim)])
|
|
420
|
+
x[tr] = xp[tr]
|
|
421
|
+
if not self.modifier is None:
|
|
422
|
+
x = self.modifier(x)
|
|
423
|
+
return x.clip(self.lower, self.upper)
|
|
424
|
+
|
|
425
|
+
def _sample(self):
|
|
426
|
+
if self.upper is None:
|
|
427
|
+
return self.rg.normal()
|
|
428
|
+
else:
|
|
429
|
+
return self.rg.uniform(self.lower, self.upper)
|
|
430
|
+
|
|
431
|
+
def _feasible(self, x):
|
|
432
|
+
if self.upper is None:
|
|
433
|
+
return x
|
|
434
|
+
else:
|
|
435
|
+
return np.clip(x, self.lower, self.upper)
|
|
436
|
+
|
|
437
|
+
# default modifier for integer variables
|
|
438
|
+
def _modifier(self, x):
|
|
439
|
+
x_ints = x[self.ints]
|
|
440
|
+
n_ints = len(self.ints)
|
|
441
|
+
lb = self.lower[self.ints]
|
|
442
|
+
ub = self.upper[self.ints]
|
|
443
|
+
to_mutate = self.rg.uniform(self.min_mutate, self.max_mutate)
|
|
444
|
+
# mututate some integer variables
|
|
445
|
+
x_ints = np.array([x if self.rg.random() > to_mutate/n_ints else
|
|
446
|
+
int(self.rg.uniform(lb[i], ub[i]))
|
|
447
|
+
for i, x in enumerate(x_ints)])
|
|
448
|
+
return x
|
|
449
|
+
|
|
450
|
+
def _is_dominated(self, y, p):
|
|
451
|
+
return np.all(np.fromiter((y[i] >= self.y[p, i] for i in range(len(y))), dtype=bool))
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _check_bounds(bounds, dim):
|
|
455
|
+
if bounds is None and dim is None:
|
|
456
|
+
raise ValueError('either dim or bounds need to be defined')
|
|
457
|
+
if bounds is None:
|
|
458
|
+
return dim, None, None
|
|
459
|
+
else:
|
|
460
|
+
return len(bounds.ub), np.asarray(bounds.lb), np.asarray(bounds.ub)
|
|
461
|
+
|
|
462
|
+
def _filter(x, y):
|
|
463
|
+
ym = np.amax(y,axis=1)
|
|
464
|
+
sorted = np.argsort(ym)
|
|
465
|
+
x = x[sorted]
|
|
466
|
+
y = y[sorted]
|
|
467
|
+
y = np.array([yi for yi in y if yi[0] < 1E99])
|
|
468
|
+
x = np.array(x[:len(y)])
|
|
469
|
+
return x,y
|
|
470
|
+
|
|
471
|
+
def objranks(objs):
|
|
472
|
+
ci = objs.argsort(axis=0)
|
|
473
|
+
rank = np.empty_like(ci)
|
|
474
|
+
ar = np.arange(objs.shape[0])
|
|
475
|
+
for i in range(objs.shape[1]):
|
|
476
|
+
rank[ci[:,i], i] = ar
|
|
477
|
+
rank = np.sum(rank, axis=1)
|
|
478
|
+
return rank
|
|
479
|
+
|
|
480
|
+
def ranks(cons, feasible, eps):
|
|
481
|
+
ci = cons.argsort(axis=0)
|
|
482
|
+
rank = np.empty_like(ci)
|
|
483
|
+
ar = np.arange(cons.shape[0])
|
|
484
|
+
for i in range(cons.shape[1]):
|
|
485
|
+
rank[ci[:,i], i] = ar
|
|
486
|
+
rank[feasible] = 0
|
|
487
|
+
alpha = np.sum(np.greater(cons, eps), axis=1) / cons.shape[1] # violations
|
|
488
|
+
alpha = np.tile(alpha, (cons.shape[1],1)).T
|
|
489
|
+
rank = rank*alpha
|
|
490
|
+
rank = np.sum(rank, axis=1)
|
|
491
|
+
return rank
|
|
492
|
+
|
|
493
|
+
def get_valid(xs, ys, nobj):
|
|
494
|
+
valid = (ys.T[nobj:].T <= 0).all(axis=1)
|
|
495
|
+
return xs[valid], ys[valid]
|
|
496
|
+
|
|
497
|
+
def pareto_sort(x0, y0, nobj, ncon):
|
|
498
|
+
domination, _, _ = pareto_domination(y0, nobj, ncon)
|
|
499
|
+
x = []
|
|
500
|
+
y = []
|
|
501
|
+
maxdom = int(max(domination))
|
|
502
|
+
for dom in range(maxdom, -1, -1):
|
|
503
|
+
domlevel = [p for p in range(len(domination)) if domination[p] == dom]
|
|
504
|
+
if len(domlevel) == 0:
|
|
505
|
+
continue
|
|
506
|
+
nx = x0[domlevel]
|
|
507
|
+
ny = y0[domlevel]
|
|
508
|
+
si = [0]
|
|
509
|
+
if len(ny) > 1:
|
|
510
|
+
cd = crowd_dist(ny)
|
|
511
|
+
si = np.flip(np.argsort(cd))
|
|
512
|
+
for p in si:
|
|
513
|
+
x.append(nx[p])
|
|
514
|
+
y.append(ny[p])
|
|
515
|
+
return np.array(x), np.array(y)
|
|
516
|
+
|
|
517
|
+
def pareto_domination(ys, nobj, ncon, last_ycon = None, last_eps = 0):
|
|
518
|
+
if ncon == 0:
|
|
519
|
+
return pareto_levels(ys), None, 0
|
|
520
|
+
else:
|
|
521
|
+
eps = 0 # adjust tolerance to small constraint violations
|
|
522
|
+
if not last_ycon is None and np.amax(last_ycon) < 1E90:
|
|
523
|
+
eps = 0.5*(last_eps + 0.5*np.mean(last_ycon, axis=0))
|
|
524
|
+
if np.amax(eps) < 1E-8: # ignore small eps
|
|
525
|
+
eps = 0
|
|
526
|
+
|
|
527
|
+
yobj = np.array([y[:nobj] for y in ys])
|
|
528
|
+
ycon = np.array([np.maximum(y[-ncon:], 0) for y in ys])
|
|
529
|
+
popn = len(ys)
|
|
530
|
+
feasible = np.less_equal(ycon, eps).all(axis=1)
|
|
531
|
+
|
|
532
|
+
csum = ranks(ycon, feasible, eps)
|
|
533
|
+
if sum(feasible) > 0:
|
|
534
|
+
csum += objranks(yobj)
|
|
535
|
+
|
|
536
|
+
ci = np.argsort(csum)
|
|
537
|
+
domination = np.zeros(popn)
|
|
538
|
+
# first pareto front of feasible solutions
|
|
539
|
+
cy = np.fromiter((i for i in ci if feasible[i]), dtype=int)
|
|
540
|
+
if len(cy) > 0:
|
|
541
|
+
ypar = pareto_levels(yobj[cy])
|
|
542
|
+
domination[cy] = ypar
|
|
543
|
+
|
|
544
|
+
# then constraint violations
|
|
545
|
+
ci = np.fromiter((i for i in ci if not feasible[i]), dtype=int)
|
|
546
|
+
if len(ci) > 0:
|
|
547
|
+
cdom = np.arange(len(ci), 0, -1)
|
|
548
|
+
domination[ci] += cdom
|
|
549
|
+
if len(cy) > 0: # priorize feasible solutions
|
|
550
|
+
domination[cy] += len(ci) + 1
|
|
551
|
+
|
|
552
|
+
return domination, ycon, eps
|
|
553
|
+
|
|
554
|
+
def pareto_levels(ys):
|
|
555
|
+
popn = len(ys)
|
|
556
|
+
pareto = np.arange(popn)
|
|
557
|
+
index = 0 # Next index to search for
|
|
558
|
+
domination = np.zeros(popn)
|
|
559
|
+
while index < len(ys):
|
|
560
|
+
mask = np.any(ys < ys[index], axis=1)
|
|
561
|
+
mask[index] = True
|
|
562
|
+
pareto = pareto[mask] # Remove dominated points
|
|
563
|
+
domination[pareto] += 1
|
|
564
|
+
ys = ys[mask]
|
|
565
|
+
index = np.sum(mask[:index])+1
|
|
566
|
+
return domination
|
|
567
|
+
|
|
568
|
+
def crowd_dist(y): # crowd distance for 1st objective
|
|
569
|
+
n = len(y)
|
|
570
|
+
y0 = np.fromiter((yi[0] for yi in y), dtype=float)
|
|
571
|
+
si = np.argsort(y0) # sort 1st objective
|
|
572
|
+
y0_s = y0[si] # sorted
|
|
573
|
+
d = y0_s[1:n] - y0_s[0:n-1] # neighbor distance
|
|
574
|
+
if max(d) == 0:
|
|
575
|
+
return np.zeros(n)
|
|
576
|
+
dsum = np.zeros(n)
|
|
577
|
+
dsum += np.array(list(d) + [0]) # distance to left
|
|
578
|
+
dsum += np.array([0] + list(d)) # distance to right
|
|
579
|
+
dsum[0] = 1E99 # keep borders
|
|
580
|
+
dsum[-1] = 1E99
|
|
581
|
+
ds = np.empty(n)
|
|
582
|
+
ds[si] = dsum # inverse order
|
|
583
|
+
return ds
|
|
584
|
+
|
|
585
|
+
# derived from https://github.com/ChengHust/NSGA-II/blob/master/GLOBAL.py
|
|
586
|
+
def variation(pop, lower, upper, rg, pro_c = 1, dis_c = 20, pro_m = 1, dis_m = 20):
|
|
587
|
+
"""Generate offspring individuals"""
|
|
588
|
+
dis_c *= 0.5 + 0.5*rg.random() # vary spread factors randomly
|
|
589
|
+
dis_m *= 0.5 + 0.5*rg.random()
|
|
590
|
+
pop = pop[:(len(pop) // 2) * 2][:]
|
|
591
|
+
(n, d) = np.shape(pop)
|
|
592
|
+
parent_1 = pop[:n // 2, :]
|
|
593
|
+
parent_2 = pop[n // 2:, :]
|
|
594
|
+
beta = np.zeros((n // 2, d))
|
|
595
|
+
mu = rg.random((n // 2, d))
|
|
596
|
+
beta[mu <= 0.5] = np.power(2 * mu[mu <= 0.5], 1 / (dis_c + 1))
|
|
597
|
+
beta[mu > 0.5] = np.power(2 * mu[mu > 0.5], -1 / (dis_c + 1))
|
|
598
|
+
beta = beta * ((-1)** rg.integers(2, size=(n // 2, d)))
|
|
599
|
+
beta[rg.random((n // 2, d)) < 0.5] = 1
|
|
600
|
+
if pro_c < 1.0:
|
|
601
|
+
beta[np.tile(rg.random((n // 2, 1)) > pro_c, (1, d))] = 1
|
|
602
|
+
parent_mean = (parent_1 + parent_2) * 0.5
|
|
603
|
+
parent_diff = (parent_1 - parent_2) * 0.5
|
|
604
|
+
offspring = np.vstack((parent_mean + beta * parent_diff, parent_mean - beta * parent_diff))
|
|
605
|
+
site = rg.random((n, d)) < pro_m / d
|
|
606
|
+
mu = rg.random((n, d))
|
|
607
|
+
temp = site & (mu <= 0.5)
|
|
608
|
+
lower, upper = np.tile(lower, (n, 1)), np.tile(upper, (n, 1))
|
|
609
|
+
norm = (offspring[temp] - lower[temp]) / (upper[temp] - lower[temp])
|
|
610
|
+
offspring[temp] += (upper[temp] - lower[temp]) * \
|
|
611
|
+
(np.power(2. * mu[temp] + (1. - 2. * mu[temp]) * np.power(1. - norm, dis_m + 1.),
|
|
612
|
+
1. / (dis_m + 1)) - 1.)
|
|
613
|
+
temp = site & (mu > 0.5)
|
|
614
|
+
norm = (upper[temp] - offspring[temp]) / (upper[temp] - lower[temp])
|
|
615
|
+
offspring[temp] += (upper[temp] - lower[temp]) * \
|
|
616
|
+
(1. - np.power(
|
|
617
|
+
2. * (1. - mu[temp]) + 2. * (mu[temp] - 0.5) * np.power(1. - norm, dis_m + 1.),
|
|
618
|
+
1. / (dis_m + 1.)))
|
|
619
|
+
offspring = np.clip(offspring, lower, upper)
|
|
620
|
+
return offspring
|
|
621
|
+
|
|
622
|
+
def feasible(xs, ys, ncon, eps = 1E-2):
|
|
623
|
+
if ncon > 0: # select feasible
|
|
624
|
+
ycon = np.array([np.maximum(y[-ncon:], 0) for y in ys])
|
|
625
|
+
con = np.sum(ycon, axis=1)
|
|
626
|
+
nobj = len(ys[0]) - ncon
|
|
627
|
+
feasible = np.fromiter((i for i in range(len(ys)) if con[i] < eps), dtype=int)
|
|
628
|
+
if len(feasible) > 0:
|
|
629
|
+
xs, ys = xs[feasible], np.array([y[:nobj] for y in ys[feasible]])
|
|
630
|
+
else:
|
|
631
|
+
print("no feasible")
|
|
632
|
+
return xs, ys
|
|
633
|
+
|
|
634
|
+
def is_feasible(y, nobj, eps = 1E-2):
|
|
635
|
+
ncon = len(y) - nobj
|
|
636
|
+
if ncon == 0:
|
|
637
|
+
return True
|
|
638
|
+
else:
|
|
639
|
+
c = np.sum(np.maximum(y[-ncon:], 0))
|
|
640
|
+
return c < eps
|
|
641
|
+
|
|
642
|
+
class wrapper(object):
|
|
643
|
+
"""thread safe wrapper for objective function monitoring evaluation count and optimization result."""
|
|
644
|
+
|
|
645
|
+
def __init__(self,
|
|
646
|
+
fun: Callable[[ArrayLike], ArrayLike],
|
|
647
|
+
nobj: int,
|
|
648
|
+
store: Optional[store] = None,
|
|
649
|
+
interval: Optional[int] = 100000,
|
|
650
|
+
plot: Optional[bool] = False,
|
|
651
|
+
name: Optional[str] = None):
|
|
652
|
+
self.fun = fun
|
|
653
|
+
self.nobj = nobj
|
|
654
|
+
self.n_evals = mp.RawValue(ct.c_long, 0)
|
|
655
|
+
self.time_0 = time.perf_counter()
|
|
656
|
+
self.best_y = mp.RawArray(ct.c_double, nobj)
|
|
657
|
+
for i in range(nobj):
|
|
658
|
+
self.best_y[i] = sys.float_info.max
|
|
659
|
+
self.store = store
|
|
660
|
+
self.interval = interval
|
|
661
|
+
self.plot = plot
|
|
662
|
+
self.name = name
|
|
663
|
+
self.lock = mp.Lock()
|
|
664
|
+
|
|
665
|
+
def __call__(self, x: ArrayLike) -> np.ndarray:
|
|
666
|
+
try:
|
|
667
|
+
y = self.fun(x)
|
|
668
|
+
with self.lock:
|
|
669
|
+
self.n_evals.value += 1
|
|
670
|
+
if not self.store is None and is_feasible(y, self.nobj):
|
|
671
|
+
self.store.create_views()
|
|
672
|
+
self.store.add_result(x, y[:self.nobj])
|
|
673
|
+
improve = False
|
|
674
|
+
for i in range(self.nobj):
|
|
675
|
+
if y[i] < self.best_y[i]:
|
|
676
|
+
improve = True
|
|
677
|
+
self.best_y[i] = y[i]
|
|
678
|
+
improve = improve# and self.n_evals.value > 10000
|
|
679
|
+
if self.n_evals.value % self.interval == 0 or improve:
|
|
680
|
+
constr = np.maximum(y[self.nobj:], 0)
|
|
681
|
+
logger.info(
|
|
682
|
+
str(dtime(self.time_0)) + ' ' +
|
|
683
|
+
str(self.n_evals.value) + ' ' +
|
|
684
|
+
str(round(self.n_evals.value/(1E-9 + dtime(self.time_0)),0)) + ' ' +
|
|
685
|
+
str(self.best_y[:]) + ' ' + str(list(constr)) + ' ' + str(list(x)))
|
|
686
|
+
if (not self.store is None) and (not self.name is None):
|
|
687
|
+
try:
|
|
688
|
+
xs, ys = self.store.get_front()
|
|
689
|
+
num = self.store.num_stored.value
|
|
690
|
+
name = self.name + '_' + str(num)
|
|
691
|
+
np.savez_compressed(name, xs=xs, ys=ys)
|
|
692
|
+
if self.plot:
|
|
693
|
+
moretry.plot(name, 0, xs, ys, all=False)
|
|
694
|
+
except Exception as ex:
|
|
695
|
+
print(str(ex))
|
|
696
|
+
return y
|
|
697
|
+
except Exception as ex:
|
|
698
|
+
print(str(ex))
|
|
699
|
+
return None
|
|
700
|
+
|
|
701
|
+
def minimize_plot(name: str,
|
|
702
|
+
fun: Callable[[ArrayLike], ArrayLike],
|
|
703
|
+
nobj: int,
|
|
704
|
+
ncon: int,
|
|
705
|
+
bounds: Bounds,
|
|
706
|
+
popsize: Optional[int] = 64,
|
|
707
|
+
max_evaluations: Optional[int] = 100000,
|
|
708
|
+
nsga_update: Optional[bool] = True,
|
|
709
|
+
pareto_update: Optional[int] = 0,
|
|
710
|
+
ints: Optional[ArrayLike] = None,
|
|
711
|
+
workers: Optional[int] = mp.cpu_count()) -> Tuple[np.ndarray, np.ndarray]:
|
|
712
|
+
name += '_mode_' + str(popsize) + '_' + \
|
|
713
|
+
('nsga_update' if nsga_update else ('de_update_' + str(pareto_update)))
|
|
714
|
+
logger.info('optimize ' + name)
|
|
715
|
+
xs, ys = minimize(fun, nobj, ncon, bounds, popsize = popsize, max_evaluations = max_evaluations,
|
|
716
|
+
nsga_update = nsga_update, pareto_update = pareto_update, workers=workers, ints=ints)
|
|
717
|
+
np.savez_compressed(name, xs=xs, ys=ys)
|
|
718
|
+
moretry.plot(name, ncon, xs, ys)
|
|
719
|
+
return xs, ys
|