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/de.py CHANGED
@@ -16,6 +16,19 @@
16
16
  You may keep parameters F and Cr at their defaults since this implementation works well with the given settings for most problems,
17
17
  since the algorithm oscillates between different F and Cr settings.
18
18
 
19
+ The filter parameter is inspired by "Surrogate-based Optimisation for a Hospital Simulation"
20
+ (https://dl.acm.org/doi/10.1145/3449726.3463283) where a machine learning classifier is used to
21
+ filter candidate solutions for DE. A filter object needs to provide function add(x, y) to enable learning and
22
+ a predicate is_improve(x, x_old, y_old) used to decide if function evaluation of x is worth the effort.
23
+
24
+ The ints parameter is a boolean array indicating which parameters are discrete integer values. This
25
+ parameter was introduced after observing non optimal results for the ESP2 benchmark problem:
26
+ https://github.com/AlgTUDelft/ExpensiveOptimBenchmark/blob/master/expensiveoptimbenchmark/problems/DockerCFDBenchmark.py
27
+ If defined it causes a "special treatment" for discrete variables: They are rounded to the next integer value and
28
+ there is an additional mutation to avoid getting stuck at local minima. This behavior is specified by the internal
29
+ function _modifier which can be overwritten by providing the optional modifier argument. If modifier is defined,
30
+ ints is ignored.
31
+
19
32
  Use the C++ implementation combined with parallel retry instead for objective functions which are fast to evaluate.
20
33
  For expensive objective functions (e.g. machine learning parameter optimization) use the workers
21
34
  parameter to parallelize objective function evaluation. This causes delayed population update.
@@ -26,25 +39,31 @@ import numpy as np
26
39
  import math, sys
27
40
  from time import time
28
41
  import ctypes as ct
29
- from fcmaes.testfun import Wrapper, Rosen, Rastrigin, Eggholder
30
- from numpy.random import Generator, MT19937
31
- from scipy.optimize import OptimizeResult
32
- from fcmaes.evaluator import Evaluator
42
+ from numpy.random import Generator, PCG64DXSM
43
+ from scipy.optimize import OptimizeResult, Bounds
44
+ from fcmaes.evaluator import Evaluator, is_debug_active
33
45
  import multiprocessing as mp
34
46
  from collections import deque
47
+ from loguru import logger
48
+ from typing import Optional, Callable, Tuple, Union
49
+ from numpy.typing import ArrayLike
35
50
 
36
- def minimize(fun,
37
- dim = None,
38
- bounds = None,
39
- popsize = 31,
40
- max_evaluations = 100000,
41
- workers = None,
42
- stop_fitness = None,
43
- keep = 200,
44
- f = 0.5,
45
- cr = 0.9,
46
- rg = Generator(MT19937()),
47
- logger = None):
51
+ def minimize(fun: Callable[[ArrayLike], float],
52
+ dim: Optional[int] = None,
53
+ bounds: Optional[Bounds] = None,
54
+ popsize: Optional[int] = 31,
55
+ max_evaluations: Optional[int] = 100000,
56
+ workers: Optional[int] = None,
57
+ stop_fitness: Optional[float] = -np.inf,
58
+ keep: Optional[int] = 200,
59
+ f: Optional[float] = 0.5,
60
+ cr: Optional[float] = 0.9,
61
+ rg: Optional[Generator] = Generator(PCG64DXSM()),
62
+ filter = None,
63
+ ints: Optional[ArrayLike] = None,
64
+ min_mutate: Optional[float] = 0.1,
65
+ max_mutate: Optional[float] = 0.5,
66
+ modifier: Optional[Callable] = None) -> OptimizeResult:
48
67
  """Minimization of a scalar function of one or more variables using
49
68
  Differential Evolution.
50
69
 
@@ -52,10 +71,8 @@ def minimize(fun,
52
71
  ----------
53
72
  fun : callable
54
73
  The objective function to be minimized.
55
- ``fun(x, *args) -> float``
56
- where ``x`` is an 1-D array with shape (n,) and ``args``
57
- is a tuple of the fixed parameters needed to completely
58
- specify the function.
74
+ ``fun(x) -> float``
75
+ where ``x`` is an 1-D array with shape (n,)
59
76
  dim : int
60
77
  dimension of the argument of the objective function
61
78
  either dim or bounds need to be defined
@@ -84,10 +101,21 @@ def minimize(fun,
84
101
  In the literature this is also known as the crossover probability.
85
102
  rg = numpy.random.Generator, optional
86
103
  Random generator for creating random guesses.
87
- logger : logger, optional
88
- logger for log output for tell_one, If None, logging
89
- is switched off. Default is a logger which logs both to stdout and
90
- appends to a file ``optimizer.log``.
104
+ filter = filter object, optional
105
+ needs to provide function add(x, y) and predicate is_improve(x, x_old, y_old).
106
+ used to decide if function evaluation of x is worth the effort.
107
+ Either f(x) < f(x_old) or f(x) < y_old need to be approximated.
108
+ add(x, y) can be used to learn from past results.
109
+ ints = list or array of bool, optional
110
+ indicating which parameters are discrete integer values. If defined these parameters will be
111
+ rounded to the next integer and some additional mutation of discrete parameters are performed.
112
+ min_mutate = float, optional
113
+ Determines the minimal mutation rate for discrete integer parameters.
114
+ max_mutate = float, optional
115
+ Determines the maximal mutation rate for discrete integer parameters.
116
+ modifier = callable, optional
117
+ used to overwrite the default behaviour induced by ints. If defined, the ints parameter is
118
+ ignored. Modifies all generated x vectors.
91
119
 
92
120
  Returns
93
121
  -------
@@ -100,7 +128,8 @@ def minimize(fun,
100
128
  ``success`` a Boolean flag indicating if the optimizer exited successfully. """
101
129
 
102
130
 
103
- de = DE(dim, bounds, popsize, stop_fitness, keep, f, cr, rg, logger)
131
+ de = DE(dim, bounds, popsize, stop_fitness, keep, f, cr, rg, filter, ints,
132
+ min_mutate, max_mutate, modifier)
104
133
  try:
105
134
  if workers and workers > 1:
106
135
  x, val, evals, iterations, stop = de.do_optimize_delayed_update(fun, max_evaluations, workers)
@@ -113,8 +142,21 @@ def minimize(fun,
113
142
 
114
143
  class DE(object):
115
144
 
116
- def __init__(self, dim, bounds, popsize = 31, stop_fitness = None, keep = 200,
117
- F = 0.5, Cr = 0.9, rg = Generator(MT19937()), logger = None):
145
+ def __init__(self,
146
+ dim: int,
147
+ bounds: Bounds,
148
+ popsize: Optional[int] = 31,
149
+ stop_fitness: Optional[float] = -np.inf,
150
+ keep: Optional[int] = 200,
151
+ F: Optional[float] = 0.5,
152
+ Cr: Optional[float] = 0.9,
153
+ rg: Optional[Generator] = Generator(PCG64DXSM()),
154
+ filter: Optional = None,
155
+ ints: Optional[ArrayLike] = None,
156
+ min_mutate: Optional[float] = 0.1,
157
+ max_mutate: Optional[float] = 0.5,
158
+ modifier: Optional[Callable] = None):
159
+
118
160
  self.dim, self.lower, self.upper = _check_bounds(bounds, dim)
119
161
  if popsize is None:
120
162
  popsize = 31
@@ -129,14 +171,24 @@ class DE(object):
129
171
  self.evals = 0
130
172
  self.p = 0
131
173
  self.improves = deque()
132
- self._init()
133
- if not logger is None:
134
- self.logger = logger
174
+ self.filter = filter
175
+ self.ints = np.array(ints)
176
+ self.min_mutate = min_mutate
177
+ self.max_mutate = max_mutate
178
+ # use default variable modifier for int variables if modifier is None
179
+ if modifier is None and not ints is None:
180
+ self.lower = self.lower.astype(float)
181
+ self.upper = self.upper.astype(float)
182
+ self.modifier = self._modifier
183
+ else:
184
+ self.modifier = modifier
185
+ self._init()
186
+ if is_debug_active():
135
187
  self.best_y = mp.RawValue(ct.c_double, 1E99)
136
188
  self.n_evals = mp.RawValue(ct.c_long, 0)
137
189
  self.time_0 = time()
138
-
139
- def ask(self):
190
+
191
+ def ask(self) -> np.ndarray:
140
192
  """ask for popsize new argument vectors.
141
193
 
142
194
  Returns
@@ -144,7 +196,7 @@ class DE(object):
144
196
  xs : popsize sized array of dim sized argument lists."""
145
197
 
146
198
  xs = [None] * self.popsize
147
- for i in range(self.popsize):
199
+ for _ in range(self.popsize):
148
200
  if self.improves:
149
201
  p, x = self.improves[0]
150
202
  if xs[p] is None:
@@ -157,9 +209,13 @@ class DE(object):
157
209
  for p in range(self.popsize):
158
210
  if xs[p] is None:
159
211
  _, _, xs[p] = self._next_x(p)
212
+ self.asked = xs
160
213
  return xs
161
214
 
162
- def tell(self, ys, xs):
215
+ def tell(self,
216
+ ys:ArrayLike,
217
+ xs:Optional[ArrayLike] = None) -> int:
218
+
163
219
  """tell function values for the argument lists retrieved by ask().
164
220
 
165
221
  Parameters
@@ -171,12 +227,22 @@ class DE(object):
171
227
  -------
172
228
  stop : int termination criteria, if != 0 loop should stop."""
173
229
 
230
+ if xs is None:
231
+ xs = self.asked
174
232
  self.evals += len(ys)
175
233
  for p in range(len(ys)):
176
234
  self.tell_one(p, ys[p], xs[p])
177
235
  return self.stop
178
236
 
179
- def ask_one(self):
237
+ def population(self) -> np.ndarray:
238
+ return self.x
239
+
240
+ def result(self) -> OptimizeResult:
241
+ return OptimizeResult(x=self.best_x, fun=self.best_value,
242
+ nfev=self.iterations*self.popsize,
243
+ nit=self.iterations, status=self.stop, success=True)
244
+
245
+ def ask_one(self) -> Tuple[int, np.ndarray]:
180
246
  """ask for one new argument vector.
181
247
 
182
248
  Returns
@@ -192,7 +258,7 @@ class DE(object):
192
258
  self.p = (self.p + 1) % self.popsize
193
259
  return p, x
194
260
 
195
- def tell_one(self, p, y, x):
261
+ def tell_one(self, p: int, y:float , x:ArrayLike) -> int:
196
262
  """tell function value for a argument list retrieved by ask_one().
197
263
 
198
264
  Parameters
@@ -204,6 +270,9 @@ class DE(object):
204
270
  Returns
205
271
  -------
206
272
  stop : int termination criteria, if != 0 loop should stop."""
273
+
274
+ if not self.filter is None:
275
+ self.filter.add(x, y)
207
276
 
208
277
  if (self.y[p] > y):
209
278
  # temporal locality
@@ -217,23 +286,23 @@ class DE(object):
217
286
  if self.best_value > y:
218
287
  self.best_x = x
219
288
  self.best_value = y
220
- if not self.stop_fitness is None and self.stop_fitness > y:
289
+ if self.stop_fitness > y:
221
290
  self.stop = 1
222
291
  self.pop_iter[p] = self.iterations
223
292
  else:
224
293
  if self.rg.uniform(0, self.keep) < self.iterations - self.pop_iter[p]:
225
294
  self.x[p] = self._sample()
226
- self.y[p] = math.inf
295
+ self.y[p] = np.inf
227
296
 
228
- if hasattr(self, 'logger'):
297
+ if is_debug_active():
229
298
  self.n_evals.value += 1
230
299
  if y < self.best_y.value or self.n_evals.value % 1000 == 999:
231
300
  if y < self.best_y.value: self.best_y.value = y
232
- t = time() - self.time_0
301
+ t = time() - self.time_0 + 1E-9
233
302
  c = self.n_evals.value
234
303
  message = '"c/t={0:.2f} c={1:d} t={2:.2f} y={3:.5f} yb={4:.5f} x={5!s}'.format(
235
304
  c/t, c, t, y, self.best_y.value, x)
236
- self.logger.info(message)
305
+ logger.debug(message)
237
306
 
238
307
  return self.stop
239
308
 
@@ -243,12 +312,25 @@ class DE(object):
243
312
  self.y = np.empty(self.popsize)
244
313
  for i in range(self.popsize):
245
314
  self.x[i] = self.x0[i] = self._sample()
246
- self.y[i] = math.inf
315
+ self.y[i] = np.inf
247
316
  self.best_x = self.x[0]
248
- self.best_value = math.inf
317
+ self.best_value = np.inf
249
318
  self.best_i = 0
250
319
  self.pop_iter = np.zeros(self.popsize)
251
320
 
321
+ def apply_fun(self, x, x_old, y_old):
322
+ if self.filter is None:
323
+ self.evals += 1
324
+ return self.fun(x)
325
+ else:
326
+ if self.filter.is_improve(x, x_old, y_old):
327
+ self.evals += 1
328
+ y = self.fun(x)
329
+ self.filter.add(x, y)
330
+ return y
331
+ else:
332
+ return 1E99
333
+
252
334
  def do_optimize(self, fun, max_evals):
253
335
  self.fun = fun
254
336
  self.max_evals = max_evals
@@ -257,14 +339,12 @@ class DE(object):
257
339
  while self.evals < self.max_evals:
258
340
  for p in range(self.popsize):
259
341
  xb, xi, x = self._next_x(p)
260
- y = self.fun(x)
261
- self.evals += 1
342
+ y = self.apply_fun(x, xi, self.y[p])
262
343
  if y < self.y[p]:
263
344
  # temporal locality
264
345
  if self.iterations > 1:
265
346
  x2 = self._next_improve(xb, x, xi)
266
- y2 = self.fun(x2)
267
- self.evals += 1
347
+ y2 = self.apply_fun(x2, x, y)
268
348
  if y2 < y:
269
349
  y = y2
270
350
  x = x2
@@ -276,13 +356,13 @@ class DE(object):
276
356
  if y < self.best_value:
277
357
  self.best_value = y;
278
358
  self.best_x = x;
279
- if not self.stop_fitness is None and self.stop_fitness > y:
359
+ if self.stop_fitness > y:
280
360
  self.stop = 1
281
361
  else:
282
362
  # reinitialize individual
283
363
  if self.rg.uniform(0, self.keep) < self.iterations - self.pop_iter[p]:
284
364
  self.x[p] = self._sample()
285
- self.y[p] = math.inf
365
+ self.y[p] = np.inf
286
366
  if self.evals >= self.max_evals:
287
367
  break
288
368
 
@@ -312,7 +392,11 @@ class DE(object):
312
392
  if self.stop != 0 or self.evals >= self.max_evals:
313
393
  break # shutdown worker if stop criteria met
314
394
 
315
- p, x = self.ask_one() # create new x
395
+ for _ in range(workers):
396
+ p, x = self.ask_one() # create new x
397
+ if self.filter is None or \
398
+ self.filter.is_improve(x, self.x[p], self.y[p]):
399
+ break
316
400
  evaluator.pipe[0].send((self.evals, x))
317
401
  evals_x[self.evals] = p, x # store x
318
402
  self.evals += 1
@@ -338,24 +422,45 @@ class DE(object):
338
422
  r = self.rg.integers(0, self.dim)
339
423
  tr = np.array(
340
424
  [i != r and self.rg.random() > self.Cr for i in range(self.dim)])
341
- x[tr] = xp[tr]
425
+ x[tr] = xp[tr]
426
+ if not self.modifier is None:
427
+ x = self.modifier(x)
342
428
  return xb, xp, x
343
429
 
344
430
  def _next_improve(self, xb, x, xi):
345
- return self._feasible(xb + ((x - xi) * 0.5))
346
-
431
+ x = self._feasible(xb + ((x - xi) * 0.5))
432
+ if not self.modifier is None:
433
+ x = self.modifier(x)
434
+ return x
435
+
347
436
  def _sample(self):
348
437
  if self.upper is None:
349
438
  return self.rg.normal()
350
439
  else:
351
- return self.rg.uniform(self.lower, self.upper)
440
+ x = self.rg.uniform(self.lower, self.upper)
441
+ if not self.modifier is None:
442
+ x = self.modifier(x)
443
+ return x
352
444
 
353
445
  def _feasible(self, x):
354
446
  if self.upper is None:
355
447
  return x
356
448
  else:
357
- return np.maximum(np.minimum(x, self.upper), self.lower)
358
-
449
+ return np.clip(x, self.lower, self.upper)
450
+
451
+ # default modifier for integer variables
452
+ def _modifier(self, x):
453
+ x_ints = x[self.ints]
454
+ n_ints = len(self.ints)
455
+ lb = self.lower[self.ints]
456
+ ub = self.upper[self.ints]
457
+ to_mutate = self.rg.uniform(self.min_mutate, self.max_mutate)
458
+ # mututate some integer variables
459
+ x[self.ints] = np.array([x if self.rg.random() > to_mutate/n_ints else
460
+ int(self.rg.uniform(lb[i], ub[i]))
461
+ for i, x in enumerate(x_ints)])
462
+ return x
463
+
359
464
  def _check_bounds(bounds, dim):
360
465
  if bounds is None and dim is None:
361
466
  raise ValueError('either dim or bounds need to be defined')
@@ -363,3 +468,5 @@ def _check_bounds(bounds, dim):
363
468
  return dim, None, None
364
469
  else:
365
470
  return len(bounds.ub), np.asarray(bounds.lb), np.asarray(bounds.ub)
471
+
472
+