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/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