fcmaes 1.3.17__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 CHANGED
@@ -2,14 +2,12 @@
2
2
  #
3
3
  # This source code is licensed under the MIT license found in the
4
4
  # LICENSE file in the root directory.
5
+ from __future__ import annotations
5
6
 
6
7
  """ Numpy based implementation of multi objective
7
- Differential Evolution using either DE/rand/1 or DE/best/1 strategy ('best' refers to the current pareto front').
8
-
9
- Can switch to NSGA-II like population update via parameter 'nsga_update'.
10
- Then it works essentially like NSGA-II but instead of the tournament selection
11
- the whole population is sorted and the best individuals survive. To do this
12
- efficiently the crowd distance ordering is slightly inaccurate.
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.
13
11
 
14
12
  Supports parallel fitness function evaluation.
15
13
 
@@ -31,37 +29,56 @@
31
29
  parameter to parallelize objective function evaluation. The workers parameter is limited by the
32
30
  population size.
33
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
+
34
40
  See https://github.com/dietmarwo/fast-cma-es/blob/master/tutorials/MODE.adoc for a detailed description.
35
41
  """
36
42
 
37
43
  import numpy as np
38
- import os
39
- import time
44
+ import os, sys, time
40
45
  import ctypes as ct
41
- from numpy.random import Generator, MT19937
42
- from fcmaes.evaluator import Evaluator
46
+ from numpy.random import Generator, PCG64DXSM
47
+ from scipy.optimize import Bounds
48
+
49
+ from fcmaes.evaluator import Evaluator, parallel_mo
43
50
  from fcmaes import moretry
44
51
  import multiprocessing as mp
45
- from fcmaes.optimizer import logger
46
- from fcmaes import moretry
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
47
57
 
48
58
  os.environ['MKL_DEBUG_CPU_TYPE'] = '5'
49
59
 
50
- def minimize(mofun,
51
- nobj,
52
- ncon,
53
- bounds,
54
- popsize = 64,
55
- max_evaluations = 100000,
56
- workers = None,
57
- f = 0.5,
58
- cr = 0.9,
59
- nsga_update = False,
60
- pareto_update = 0,
61
- rg = Generator(MT19937()),
62
- logger = None,
63
- plot_name = None,
64
- store = None):
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]:
65
82
 
66
83
  """Minimization of a multi objjective function of one or more variables using
67
84
  Differential Evolution.
@@ -70,10 +87,8 @@ def minimize(mofun,
70
87
  ----------
71
88
  mofun : callable
72
89
  The objective function to be minimized.
73
- ``mofun(x, *args) -> list(float)``
74
- where ``x`` is an 1-D array with shape (n,) and ``args``
75
- is a tuple of the fixed parameters needed to completely
76
- specify the function.
90
+ ``mofun(x) -> ndarray(float)``
91
+ where ``x`` is an 1-D array with shape (n,)
77
92
  nobj : int
78
93
  number of objectives
79
94
  ncon : int
@@ -84,33 +99,40 @@ def minimize(mofun,
84
99
  1. Instance of the `scipy.Bounds` class.
85
100
  2. Sequence of ``(min, max)`` pairs for each element in `x`. None
86
101
  is used to specify no bound.
102
+ guess : ndarray, shape (popsize,dim) or Tuple
103
+ Initial guess.
87
104
  popsize : int, optional
88
105
  Population size.
89
106
  max_evaluations : int, optional
90
- Forced termination after ``max_evaluations`` function evaluations.
107
+ Forced termination after ``max_evaluations`` function evaluations.
91
108
  workers : int or None, optional
92
- If not workers is None, function evaluation is performed in parallel for the whole population.
109
+ workers > 1, function evaluation is performed in parallel for the whole population.
93
110
  Useful for costly objective functions
94
111
  f = float, optional
95
112
  The mutation constant. In the literature this is also known as differential weight,
96
113
  being denoted by F. Should be in the range [0, 2].
97
114
  cr = float, optional
98
115
  The recombination constant. Should be in the range [0, 1].
99
- In the literature this is also known as the crossover probability.
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.
100
119
  nsga_update = boolean, optional
101
- Use of NSGA-II or DE population update. Default is False
120
+ Use of NSGA-II/SBX or DE population update. Default is True
102
121
  pareto_update = float, optional
103
- Only applied if nsga_update = False. Use the pareto front for population update
104
- with probability pareto_update, else use the whole population. Default 0 - use always
105
- the whole population.
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.
106
134
  rg = numpy.random.Generator, optional
107
135
  Random generator for creating random guesses.
108
- logger : logger, optional
109
- logger for log output for tell_one, If None, logging
110
- is switched off. Default is a logger which logs both to stdout and
111
- appends to a file ``optimizer.log``.
112
- plot_name : plot_name, optional
113
- if defined the pareto front is plotted during the optimization to monitor progress
114
136
  store : result store, optional
115
137
  if defined the optimization results are added to the result store. For multi threaded execution.
116
138
  use workers=1 if you call minimize from multiple threads
@@ -119,17 +141,19 @@ def minimize(mofun,
119
141
  -------
120
142
  x, y: list of argument vectors and corresponding value vectors of the optimization results. """
121
143
 
122
- mode = MODE(nobj, ncon, bounds, popsize, workers if not workers is None else 0,
123
- f, cr, nsga_update, pareto_update, rg, logger, plot_name)
124
- try:
125
- if workers and workers > 1:
126
- x, y, evals, iterations, stop = mode.do_optimize_delayed_update(mofun, max_evaluations, workers)
127
- else:
128
- x, y, evals, iterations, stop = mode.do_optimize(mofun, max_evaluations)
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)
129
152
  if not store is None:
130
153
  store.add_results(x, y)
131
154
  return x, y
132
155
  except Exception as ex:
156
+ print(str(ex))
133
157
  return None, None
134
158
 
135
159
 
@@ -144,7 +168,8 @@ class store():
144
168
  nobj : int
145
169
  number of objectives
146
170
  capacity : int, optional
147
- capacity of the result store.
171
+ capacity of the store collecting all solutions. If full, its content is replaced by its
172
+ pareto front.
148
173
  """
149
174
 
150
175
  def __init__(self, dim, nobj, capacity = mp.cpu_count()*512):
@@ -152,58 +177,83 @@ class store():
152
177
  self.nobj = nobj
153
178
  self.capacity = capacity
154
179
  self.add_mutex = mp.Lock()
155
- self.xs = mp.RawArray(ct.c_double, self.capacity * self.dim)
156
- self.ys = mp.RawArray(ct.c_double, self.capacity * self.nobj)
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()
157
183
  self.num_stored = mp.RawValue(ct.c_int, 0)
158
184
  self.num_added = mp.RawValue(ct.c_int, 0)
159
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
+
160
205
  def add_results(self, xs, ys):
161
206
  with self.add_mutex:
162
207
  self.num_added.value += 1
163
208
  i = self.num_stored.value
164
209
  for j in range(len(xs)):
165
- if i < self.capacity:
166
- self.set_x(i, xs[j])
167
- self.set_y(i, ys[j])
210
+ if i < self.capacity:
211
+ self.xs_view[i] = xs[j]
212
+ self.ys_view[i] = ys[j][:self.nobj]
168
213
  i += 1
169
- else: # store is full, replace with pareto front
170
- xf, yf = self.get_front()
171
- i = 0
172
- for k in range(len(xf)):
173
- self.set_x(i, xf[k])
174
- self.set_y(i, yf[k])
175
- i += 1
176
- if i == self.capacity:
177
- break
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
178
219
  self.num_stored.value = i
179
220
 
180
- def get_front(self):
181
- return moretry.pareto(self.get_xs(), self.get_ys())
182
-
183
- def get_xs(self):
184
- return np.array([self.get_x(i) for i in range(self.num_stored.value)])
185
-
186
- def get_ys(self):
187
- return np.array([self.get_y(i) for i in range(self.num_stored.value)])
188
-
189
- def get_x(self, i):
190
- return self.xs[i*self.dim:(i+1)*self.dim]
191
-
192
- def set_x(self, i, x):
193
- self.xs[i*self.dim:(i+1)*self.dim] = x[:]
194
-
195
- def get_y(self, i):
196
- return self.ys[i*self.nobj:(i+1)*self.nobj]
197
-
198
- def set_y(self, i, y):
199
- self.ys[i*self.nobj:(i+1)*self.nobj] = y[:]
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
200
232
 
233
+ def get_content(self):
234
+ stored = self.num_stored.value
235
+ return self.xs_view[:stored], self.ys_view[:stored]
201
236
 
202
237
  class MODE(object):
203
238
 
204
- def __init__(self, nobj, ncon, bounds, popsize = 64, workers = 0,
205
- F = 0.5, Cr = 0.9, nsga_update = False, pareto_update = False,
206
- rg = Generator(MT19937()), logger = None, plot_name = None):
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):
207
257
  self.nobj = nobj
208
258
  self.ncon = ncon
209
259
  self.dim, self.lower, self.upper = _check_bounds(bounds, None)
@@ -212,10 +262,13 @@ class MODE(object):
212
262
  if popsize % 2 == 1 and nsga_update: # nsga update requires even popsize
213
263
  popsize += 1
214
264
  self.popsize = popsize
215
- self.workers = workers
216
265
  self.rg = rg
217
266
  self.F0 = F
218
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
219
272
  self.nsga_update = nsga_update
220
273
  self.pareto_update = pareto_update
221
274
  self.stop = 0
@@ -223,154 +276,103 @@ class MODE(object):
223
276
  self.evals = 0
224
277
  self.mutex = mp.Lock()
225
278
  self.p = 0
226
- self.best_p = None
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
227
290
  self._init()
228
- if not logger is None:
229
- self.logger = logger
230
- self.n_evals = mp.RawValue(ct.c_long, 0)
231
- self.time_0 = time.perf_counter()
232
- if not plot_name is None:
233
- self.plot_name = plot_name + '_mode_' + str(popsize) + '_' + \
234
- ('nsga_update' if nsga_update else ('de_update_' + str(pareto_update)))
235
-
236
- def ask(self):
237
- """ask for one new argument vector.
238
-
239
- Returns
240
- -------
241
- p : int population index
242
- x : dim sized argument ."""
243
-
244
- p = self.p
245
- x = self._next_x(p)
246
- self.p = (self.p + 1) % self.popsize
247
- return p, x
248
-
249
- def tell(self, p, y, x):
250
- """tell function value for a argument list retrieved by ask_one().
251
-
252
- Parameters
253
- ----------
254
- p : int population index
255
- y : function value
256
- x : dim sized argument list
257
-
258
- Returns
259
- -------
260
- stop : int termination criteria, if != 0 loop should stop."""
261
-
262
- with self.mutex:
263
- for dp in range(len(self.done)):
264
- if not self.done[dp]:
265
- break
266
- self.nx[dp] = x
267
- self.ny[dp] = y
268
- self.done[dp] = True
269
- if sum(self.done) >= self.popsize:
270
- done_p = np.arange(len(self.ny))
271
- done_p = done_p[self.done]
272
- p = self.popsize
273
- for dp in done_p:
274
- self.x[p] = self.nx[dp]
275
- self.y[p] = self.ny[dp]
276
- self.done[dp] = False
277
- if p >= len(self.y):
278
- break
279
- p += 1
280
- self.pop_update()
281
-
282
- if hasattr(self, 'logger'):
283
- self.n_evals.value += 1
284
- if self.n_evals.value % 100000 == 99999:
285
- t = time.perf_counter() - self.time_0
286
- c = self.n_evals.value
287
- message = '"c/t={0:.2f} c={1:d} t={2:.2f} y={3!s} x={4!s}'.format(
288
- c/t, c, t, str(list(y)), str(list(x)))
289
- self.logger.info(message)
290
- if hasattr(self, "plot_name") and not self.plot_name is None:
291
- name = self.plot_name + ' ' + str(self.n_evals.value)
292
- np.savez_compressed(name, xs=self.x, ys=self.y)
293
- moretry.plot(name, self.ncon, self.x, self.y)
294
-
295
- return self.stop
296
-
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
+
297
317
  def _init(self):
298
318
  self.x = np.empty((2*self.popsize, self.dim))
299
319
  self.y = np.empty((2*self.popsize, self.nobj + self.ncon))
300
320
  for i in range(self.popsize):
301
321
  self.x[i] = self._sample()
302
322
  self.y[i] = np.array([1E99]*(self.nobj + self.ncon))
303
-
304
- next_size = 2*(max(self.workers, self.popsize))
305
- self.done = np.zeros(next_size, dtype=bool)
306
- self.nx = np.empty((next_size, self.dim))
307
- self.ny = np.empty((next_size, self.nobj + self.ncon))
308
323
  self.vx = self.x.copy()
309
324
  self.vp = 0
310
-
311
- def do_optimize(self, fun, max_evals):
312
- self.fun = fun
313
- self.max_evals = max_evals
314
- self.iterations = 0
315
- self.evals = 0
316
- while self.evals < self.max_evals:
317
- for p in range(self.popsize):
318
- x = self._next_x(p)
319
- self.y[self.popsize + p] = self.fun(x)
320
- self.x[self.popsize + p] = x
321
- self.evals += 1
322
- self.pop_update()
323
- x, y = filter(self.x, self.y)
324
- return x, y, self.evals, self.iterations, self.stop
325
+ self.ycon = None
326
+ self.eps = 0
325
327
 
326
- def do_optimize_delayed_update(self, fun, max_evals, workers=mp.cpu_count()):
327
- self.fun = fun
328
- self.max_evals = max_evals
329
- evaluator = Evaluator(self.fun)
330
- evaluator.start(workers)
331
- evals_x = {}
332
- self.iterations = 0
333
- self.evals = 0
334
- self.p = 0
335
- for _ in range(workers): # fill queue with initial population
336
- p, x = self.ask()
337
- evaluator.pipe[0].send((self.evals, x))
338
- evals_x[self.evals] = p, x # store x
339
- self.evals += 1
340
-
341
- while True: # read from pipe, tell de and create new x
342
- evals, y = evaluator.pipe[0].recv()
343
- p, x = evals_x[evals] # retrieve evaluated x
344
- del evals_x[evals]
345
- self.tell(p, y, x) # tell evaluated x
346
- if self.stop != 0 or self.evals >= self.max_evals:
347
- break # shutdown worker if stop criteria met
348
-
349
- p, x = self.ask() # create new x
350
- evaluator.pipe[0].send((self.evals, x))
351
- evals_x[self.evals] = p, x # store x
352
- self.evals += 1
353
-
354
- evaluator.stop()
355
- x, y = filter(self.x, self.y)
356
- return x, y, self.evals, self.iterations, self.stop
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
357
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
+
358
354
  def pop_update(self):
359
- domination = pareto(self.y, self.nobj, self.ncon)
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)
360
362
  x = []
361
363
  y = []
362
364
  maxdom = int(max(domination))
363
365
  for dom in range(maxdom, -1, -1):
364
366
  domlevel = [p for p in range(len(domination)) if domination[p] == dom]
365
- if dom == maxdom: # store pareto front in self.best_p
366
- self.best_p = domlevel
367
+ if len(domlevel) == 0:
368
+ continue
367
369
  if len(x) + len(domlevel) <= self.popsize:
368
370
  # whole level fits
369
- x = [*x, *self.x[domlevel]]
370
- y = [*y, *self.y[domlevel]]
371
+ x = [*x, *x0[domlevel]]
372
+ y = [*y, *y0[domlevel]]
371
373
  else: # sort for crowding
372
- nx = self.x[domlevel]
373
- ny = self.y[domlevel]
374
+ nx = x0[domlevel]
375
+ ny = y0[domlevel]
374
376
  si = [0]
375
377
  if len(ny) > 1:
376
378
  cd = crowd_dist(ny)
@@ -384,7 +386,8 @@ class MODE(object):
384
386
  self.x[:self.popsize] = x[:self.popsize]
385
387
  self.y[:self.popsize] = y[:self.popsize]
386
388
  if self.nsga_update:
387
- self.vx = variation(self.x[:self.popsize], self.lower, self.upper, self.rg)
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)
388
391
 
389
392
  def _next_x(self, p):
390
393
  if self.nsga_update: # use NSGA-II update strategy.
@@ -397,17 +400,15 @@ class MODE(object):
397
400
  self.Cr = 0.5*self.Cr0 if self.iterations % 2 == 0 else self.Cr0
398
401
  self.F = 0.5*self.F0 if self.iterations % 2 == 0 else self.F0
399
402
  while True:
400
- if self.rg.random() < self.pareto_update and (not self.best_p is None) and len(self.best_p) > 3:
401
- # sample from pareto front
402
- rb = self.rg.integers(0, len(self.best_p))
403
- rb = self.best_p[rb]
403
+ if self.pareto_update > 0: # sample elite solutions
404
404
  r1, r2 = self.rg.integers(0, self.popsize, 2)
405
+ rb = int(self.popsize * (self.rg.random() ** (1.0 + self.pareto_update)))
405
406
  else:
406
407
  # sample from whole population
407
408
  r1, r2, rb = self.rg.integers(0, self.popsize, 3)
408
- if r1 != p and r1 != rb and r1 != r2 and r2 != rb \
409
- and r2 != p and rb != p:
410
- break
409
+ if r1 != p and r1 != rb and r1 != r2 and r2 != rb \
410
+ and r2 != p and rb != p:
411
+ break
411
412
  xp = self.x[p]
412
413
  xb = self.x[rb]
413
414
  x1 = self.x[r1]
@@ -416,8 +417,10 @@ class MODE(object):
416
417
  r = self.rg.integers(0, self.dim)
417
418
  tr = np.array(
418
419
  [i != r and self.rg.random() > self.Cr for i in range(self.dim)])
419
- x[tr] = xp[tr]
420
- return x
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)
421
424
 
422
425
  def _sample(self):
423
426
  if self.upper is None:
@@ -429,7 +432,24 @@ class MODE(object):
429
432
  if self.upper is None:
430
433
  return x
431
434
  else:
432
- return np.maximum(np.minimum(x, self.upper), self.lower)
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
+
433
453
 
434
454
  def _check_bounds(bounds, dim):
435
455
  if bounds is None and dim is None:
@@ -438,8 +458,8 @@ def _check_bounds(bounds, dim):
438
458
  return dim, None, None
439
459
  else:
440
460
  return len(bounds.ub), np.asarray(bounds.lb), np.asarray(bounds.ub)
441
-
442
- def filter(x, y):
461
+
462
+ def _filter(x, y):
443
463
  ym = np.amax(y,axis=1)
444
464
  sorted = np.argsort(ym)
445
465
  x = x[sorted]
@@ -457,47 +477,79 @@ def objranks(objs):
457
477
  rank = np.sum(rank, axis=1)
458
478
  return rank
459
479
 
460
- def ranks(cons):
461
- feasible = np.less_equal(cons, 0)
480
+ def ranks(cons, feasible, eps):
462
481
  ci = cons.argsort(axis=0)
463
482
  rank = np.empty_like(ci)
464
483
  ar = np.arange(cons.shape[0])
465
484
  for i in range(cons.shape[1]):
466
485
  rank[ci[:,i], i] = ar
467
486
  rank[feasible] = 0
468
- alpha = np.sum(np.greater(cons, 0), axis=1) / cons.shape[1] # violations
487
+ alpha = np.sum(np.greater(cons, eps), axis=1) / cons.shape[1] # violations
469
488
  alpha = np.tile(alpha, (cons.shape[1],1)).T
470
489
  rank = rank*alpha
471
490
  rank = np.sum(rank, axis=1)
472
491
  return rank
473
-
474
- def pareto(ys, nobj, ncon):
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):
475
518
  if ncon == 0:
476
- return pareto_levels(ys)
519
+ return pareto_levels(ys), None, 0
477
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
+
478
527
  yobj = np.array([y[:nobj] for y in ys])
479
- ycon = np.array([np.maximum(y[-ncon:], 0) for y in ys])
480
- csum = ranks(ycon)
481
- feasible = np.less_equal(csum, 0)
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)
482
533
  if sum(feasible) > 0:
483
534
  csum += objranks(yobj)
535
+
484
536
  ci = np.argsort(csum)
485
- popn = len(ys)
486
537
  domination = np.zeros(popn)
487
538
  # first pareto front of feasible solutions
488
- cy = np.array([i for i in ci if feasible[i]])
539
+ cy = np.fromiter((i for i in ci if feasible[i]), dtype=int)
489
540
  if len(cy) > 0:
490
541
  ypar = pareto_levels(yobj[cy])
491
- domination[cy] += ypar
542
+ domination[cy] = ypar
543
+
492
544
  # then constraint violations
493
- ci = np.array([i for i in ci if not feasible[i]])
545
+ ci = np.fromiter((i for i in ci if not feasible[i]), dtype=int)
494
546
  if len(ci) > 0:
495
- maxcdom = len(ci)
496
- cdom = np.arange(maxcdom, 0, -1)
547
+ cdom = np.arange(len(ci), 0, -1)
497
548
  domination[ci] += cdom
498
549
  if len(cy) > 0: # priorize feasible solutions
499
- domination[cy] += maxcdom + 1
500
- return domination
550
+ domination[cy] += len(ci) + 1
551
+
552
+ return domination, ycon, eps
501
553
 
502
554
  def pareto_levels(ys):
503
555
  popn = len(ys)
@@ -515,7 +567,7 @@ def pareto_levels(ys):
515
567
 
516
568
  def crowd_dist(y): # crowd distance for 1st objective
517
569
  n = len(y)
518
- y0 = np.array([yi[0] for yi in y])
570
+ y0 = np.fromiter((yi[0] for yi in y), dtype=float)
519
571
  si = np.argsort(y0) # sort 1st objective
520
572
  y0_s = y0[si] # sorted
521
573
  d = y0_s[1:n] - y0_s[0:n-1] # neighbor distance
@@ -533,6 +585,8 @@ def crowd_dist(y): # crowd distance for 1st objective
533
585
  # derived from https://github.com/ChengHust/NSGA-II/blob/master/GLOBAL.py
534
586
  def variation(pop, lower, upper, rg, pro_c = 1, dis_c = 20, pro_m = 1, dis_m = 20):
535
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()
536
590
  pop = pop[:(len(pop) // 2) * 2][:]
537
591
  (n, d) = np.shape(pop)
538
592
  parent_1 = pop[:n // 2, :]
@@ -545,8 +599,9 @@ def variation(pop, lower, upper, rg, pro_c = 1, dis_c = 20, pro_m = 1, dis_m = 2
545
599
  beta[rg.random((n // 2, d)) < 0.5] = 1
546
600
  if pro_c < 1.0:
547
601
  beta[np.tile(rg.random((n // 2, 1)) > pro_c, (1, d))] = 1
548
- offspring = np.vstack(((parent_1 + parent_2) / 2 + beta * (parent_1 - parent_2) / 2,
549
- (parent_1 + parent_2) / 2 - beta * (parent_1 - parent_2) / 2))
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))
550
605
  site = rg.random((n, d)) < pro_m / d
551
606
  mu = rg.random((n, d))
552
607
  temp = site & (mu <= 0.5)
@@ -561,16 +616,104 @@ def variation(pop, lower, upper, rg, pro_c = 1, dis_c = 20, pro_m = 1, dis_m = 2
561
616
  (1. - np.power(
562
617
  2. * (1. - mu[temp]) + 2. * (mu[temp] - 0.5) * np.power(1. - norm, dis_m + 1.),
563
618
  1. / (dis_m + 1.)))
564
- offspring = np.maximum(np.minimum(offspring, upper), lower)
619
+ offspring = np.clip(offspring, lower, upper)
565
620
  return offspring
566
621
 
567
- def minimize_plot(name, fun, nobj, ncon, bounds, popsize = 64, max_evaluations = 100000, nsga_update=False,
568
- pareto_update=0, workers = mp.cpu_count(), logger=logger(), plot_name = None):
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]:
569
712
  name += '_mode_' + str(popsize) + '_' + \
570
713
  ('nsga_update' if nsga_update else ('de_update_' + str(pareto_update)))
571
714
  logger.info('optimize ' + name)
572
715
  xs, ys = minimize(fun, nobj, ncon, bounds, popsize = popsize, max_evaluations = max_evaluations,
573
- nsga_update = nsga_update, pareto_update = pareto_update, workers=workers,
574
- logger=logger, plot_name = plot_name)
716
+ nsga_update = nsga_update, pareto_update = pareto_update, workers=workers, ints=ints)
575
717
  np.savez_compressed(name, xs=xs, ys=ys)
576
718
  moretry.plot(name, ncon, xs, ys)
719
+ return xs, ys