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/cmaes.py CHANGED
@@ -12,36 +12,43 @@ import sys
12
12
  import os
13
13
  import math
14
14
  import numpy as np
15
+ from time import time
16
+ import ctypes as ct
17
+ import multiprocessing as mp
15
18
  from scipy import linalg
16
- from scipy.optimize import OptimizeResult
17
- from numpy.random import MT19937, Generator
18
- from fcmaes.evaluator import Evaluator, eval_parallel
19
+ from scipy.optimize import OptimizeResult, Bounds
20
+ from numpy.random import PCG64DXSM, Generator
21
+ from fcmaes.evaluator import Evaluator, serial, _check_bounds, _fitness, is_debug_active
22
+
23
+ from loguru import logger
24
+ from typing import Optional, Callable, Union
25
+ from numpy.typing import ArrayLike
19
26
 
20
27
  os.environ['MKL_DEBUG_CPU_TYPE'] = '5'
21
28
 
22
- def minimize(fun,
23
- bounds=None,
24
- x0=None,
25
- input_sigma = 0.3,
26
- popsize = 31,
27
- max_evaluations = 100000,
28
- max_iterations = 100000,
29
- workers = None,
30
- accuracy = 1.0,
31
- stop_fittness = np.nan,
32
- is_terminate = None,
33
- rg = Generator(MT19937()),
34
- runid=0):
29
+ def minimize(fun: Callable[[ArrayLike], float],
30
+ bounds: Optional[Bounds] = None,
31
+ x0: Optional[ArrayLike] = None,
32
+ input_sigma: Optional[Union[float, ArrayLike, Callable]] = 0.3,
33
+ popsize: Optional[int] = 31,
34
+ max_evaluations: Optional[int] = 100000,
35
+ max_iterations: Optional[int] = 100000,
36
+ workers: Optional[int] = 1,
37
+ accuracy: Optional[float] = 1.0,
38
+ stop_fitness: Optional[float] = -np.inf,
39
+ is_terminate: Optional[Callable[[ArrayLike, float], bool]] = None,
40
+ rg: Optional[Generator] = Generator(PCG64DXSM()),
41
+ runid: Optional[int] = 0,
42
+ normalize: Optional[bool] = True,
43
+ update_gap: Optional[int] = None) -> OptimizeResult:
35
44
  """Minimization of a scalar function of one or more variables using CMA-ES.
36
45
 
37
46
  Parameters
38
47
  ----------
39
48
  fun : callable
40
49
  The objective function to be minimized.
41
- ``fun(x, *args) -> float``
42
- where ``x`` is an 1-D array with shape (n,) and ``args``
43
- is a tuple of the fixed parameters needed to completely
44
- specify the function.
50
+ ``fun(x) -> float``
51
+ where ``x`` is an 1-D array with shape (n,)
45
52
  bounds : sequence or `Bounds`, optional
46
53
  Bounds on variables. There are two ways to specify the bounds:
47
54
  1. Instance of the `scipy.Bounds` class.
@@ -59,11 +66,11 @@ def minimize(fun,
59
66
  max_iterations : int, optional
60
67
  Forced termination after ``max_iterations`` iterations.
61
68
  workers : int or None, optional
62
- If not workers is None, function evaluation is performed in parallel for the whole population.
69
+ If workers > 1, function evaluation is performed in parallel for the whole population.
63
70
  Useful for costly objective functions but is deactivated for parallel retry.
64
71
  accuracy : float, optional
65
72
  values > 1.0 reduce the accuracy.
66
- stop_fittness : float, optional
73
+ stop_fitness : float, optional
67
74
  Limit for fitness value. If reached minimize terminates.
68
75
  is_terminate : callable, optional
69
76
  Callback to be used if the caller of minimize wants to
@@ -72,7 +79,11 @@ def minimize(fun,
72
79
  Random generator for creating random guesses.
73
80
  runid : int, optional
74
81
  id used by the is_terminate callback to identify the CMA-ES run.
75
-
82
+ normalize : boolean, optional
83
+ pheno -> if true geno transformation maps arguments to interval [-1,1]
84
+ update_gap : int, optional
85
+ number of iterations without distribution update
86
+
76
87
  Returns
77
88
  -------
78
89
  res : scipy.OptimizeResult
@@ -81,42 +92,47 @@ def minimize(fun,
81
92
  ``fun`` the best function value, ``nfev`` the number of function evaluations,
82
93
  ``nit`` the number of CMA-ES iterations, ``status`` the stopping critera and
83
94
  ``success`` a Boolean flag indicating if the optimizer exited successfully. """
84
-
85
- fun = serial(fun) if workers is None else parallel(fun, workers)
95
+
96
+ if workers is None or workers <= 1:
97
+ fun = serial(fun)
86
98
  cmaes = Cmaes(bounds, x0,
87
- input_sigma, popsize,
88
- max_evaluations, max_iterations,
89
- accuracy, stop_fittness,
90
- is_terminate, rg, np.random.randn, runid, fun)
91
- x, val, evals, iterations, stop = cmaes.doOptimize()
92
- if not workers is None:
93
- fun.stop() # stop all parallel evaluation processes
99
+ input_sigma, popsize,
100
+ max_evaluations, max_iterations,
101
+ accuracy, stop_fitness,
102
+ is_terminate, rg, np.random.randn, runid, normalize,
103
+ update_gap, fun)
104
+ if workers and workers > 1:
105
+ x, val, evals, iterations, stop = cmaes.do_optimize_delayed_update(fun, workers=workers)
106
+ else:
107
+ x, val, evals, iterations, stop = cmaes.doOptimize()
94
108
  return OptimizeResult(x=x, fun=val, nfev=evals, nit=iterations, status=stop,
95
109
  success=True)
96
110
 
97
111
  class Cmaes(object):
98
112
  """Implements the cma-es ask/tell interactive interface."""
99
113
 
100
- def __init__(self, bounds=None,
101
- x0=None,
102
- input_sigma = 0.3,
103
- popsize = 31,
104
- max_evaluations = 100000,
105
- max_iterations = 100000,
106
- accuracy = 1.0,
107
- stop_fittness = np.nan,
108
- is_terminate = None,
109
- rg = Generator(MT19937()), # used if x0 is undefined
110
- randn = np.random.randn, # used for random offspring
111
- runid=0,
112
- fun = None
114
+ def __init__(self, bounds: Optional[Bounds] = None,
115
+ x0: Optional[ArrayLike] = None,
116
+ input_sigma: Optional[Union[float, ArrayLike, Callable]] = 0.3,
117
+ popsize: Optional[int] = 31,
118
+ max_evaluations: Optional[int] = 100000,
119
+ max_iterations: Optional[int] = 100000,
120
+ accuracy: Optional[int] = 1.0,
121
+ stop_fitness: Optional[float] = -np.inf,
122
+ is_terminate: Optional[bool] = None,
123
+ rg: Optional[Generator] = Generator(PCG64DXSM()), # used if x0 is undefined
124
+ randn: Optional[Callable] = np.random.randn, # used for random offspring
125
+ runid: Optional[int] = 0,
126
+ normalize: Optional[bool] = True,
127
+ update_gap: Optional[int] = None,
128
+ fun: Optional[Callable[[ArrayLike], float]] = None
113
129
  ):
114
130
 
115
131
  # runid used in is_terminate callback to identify a specific run at different iteration
116
132
  self.runid = runid
117
133
  # bounds and guess
118
134
  lower, upper, guess = _check_bounds(bounds, x0, rg)
119
- self.fitfun = _Fittness(fun, lower, upper)
135
+ self.fitfun = _fitness(fun, lower, upper, normalize)
120
136
  # initial guess for the arguments of the fitness function
121
137
  self.guess = self.fitfun.encode(guess)
122
138
  # random generators
@@ -140,6 +156,8 @@ class Cmaes(object):
140
156
  # Individual sigma values - initial search volume. input_sigma determines
141
157
  # the initial coordinate wise standard deviations for the search. Setting
142
158
  # SIGMA one third of the initial search region is appropriate.
159
+ if callable(input_sigma):
160
+ input_sigma=input_sigma()
143
161
  if isinstance(input_sigma, list):
144
162
  self.insigma = np.asarray(input_sigma)
145
163
  elif np.isscalar(input_sigma):
@@ -153,7 +171,7 @@ class Cmaes(object):
153
171
  self.max_evaluations = max_evaluations
154
172
  self.max_iterations = max_iterations
155
173
  # Limit for fitness value.
156
- self.stop_fitness = stop_fittness
174
+ self.stop_fitness = stop_fitness
157
175
  # Stop if x-changes larger stopTolUpX.
158
176
  self.stopTolUpX = 1e3 * self.sigma
159
177
  # Stop if x-change smaller stopTolX.
@@ -165,6 +183,12 @@ class Cmaes(object):
165
183
  # selection strategy parameters
166
184
  # Number of parents/points for recombination.
167
185
  self.mu = int(self.popsize/2)
186
+ # timing / global best value
187
+ if is_debug_active():
188
+ self.best_y = mp.RawValue(ct.c_double, 1E99)
189
+ self.n_evals = mp.RawValue(ct.c_long, 0)
190
+ self.time_0 = time()
191
+
168
192
  # Array for weighted recombination.
169
193
  self.weights = (np.log(np.arange(1, self.mu+1, 1)) * -1) + math.log(self.mu + 0.5)
170
194
  sumw = np.sum(self.weights)
@@ -190,6 +214,9 @@ class Cmaes(object):
190
214
  self.chiN = math.sqrt(self.dim) * (1. - 1. / (4. * self.dim) + 1 / (21. * self.dim * self.dim))
191
215
  self.ccov1Sep = min(1., self.ccov1 * (self.dim + 1.5) / 3.)
192
216
  self.ccovmuSep = min(1. - self.ccov1, self.ccovmu * (self.dim + 1.5) / 3.)
217
+ # lazy covariance update gap
218
+ self.lazy_update_gap = 1. / (self.ccov1 + self.ccovmu + 1e-23) / self.dim / 10 \
219
+ if update_gap is None else update_gap
193
220
 
194
221
  # CMA internal values - updated each generation
195
222
  # Objective variables.
@@ -215,6 +242,7 @@ class Cmaes(object):
215
242
  self.historySize = 10 + int(3. * 10. * self.dim / popsize)
216
243
 
217
244
  self.iterations = 0
245
+ self.last_update = 0
218
246
  self.stop = 0
219
247
  self.best_value = sys.float_info.max
220
248
  self.best_x = None
@@ -222,9 +250,9 @@ class Cmaes(object):
222
250
  self.fitness_history = np.full(self.historySize, sys.float_info.max)
223
251
  self.fitness_history[0] = self.best_value
224
252
  self.arz = None
253
+ self.fitness = None
225
254
 
226
-
227
- def ask(self):
255
+ def ask(self) -> np.array:
228
256
  """ask for popsize new argument vectors.
229
257
 
230
258
  Returns
@@ -232,9 +260,11 @@ class Cmaes(object):
232
260
  xs : popsize sized list of dim sized argument lists."""
233
261
 
234
262
  self.newArgs()
235
- return [self.fitfun.decode(x) for x in self.arx]
263
+ return np.array([self.fitfun.decode(x) for x in self.arx])
236
264
 
237
- def tell(self, ys, xs = None):
265
+ def tell(self,
266
+ ys: np.ndarray,
267
+ xs: Optional[np.ndarray] = None) -> int:
238
268
  """tell function values for the argument lists retrieved by ask().
239
269
 
240
270
  Parameters
@@ -264,15 +294,106 @@ class Cmaes(object):
264
294
  self.updateCMA()
265
295
  self.arz = None
266
296
  return self.stop
267
-
297
+
298
+ def population(self) -> np.array:
299
+ return self.fitfun.decode(self.arx)
300
+
301
+ def result(self) -> OptimizeResult:
302
+ return OptimizeResult(x=self.best_x, fun=self.best_value,
303
+ nfev=self.fitfun.evaluation_counter,
304
+ nit=self.iterations, status=self.stop, success=True)
305
+
306
+ def ask_one(self) -> np.array:
307
+ """ask for one new argument vector.
308
+
309
+ Returns
310
+ -------
311
+ x : dim sized argument ."""
312
+ arz = self.randn(self.dim)
313
+ delta = (self.BD @ arz.transpose()) * self.sigma
314
+ arx = self.fitfun.closestFeasible(self.xmean + delta.transpose())
315
+ return self.fitfun.decode(arx)
316
+
317
+ def tell_one(self,
318
+ y: float,
319
+ x: np.array) -> int:
320
+ """tell function value for a argument list retrieved by ask_one().
321
+
322
+ Parameters
323
+ ----------
324
+ y : function value
325
+ x : dim sized argument list
326
+
327
+ Returns
328
+ -------
329
+ stop : int termination criteria, if != 0 loop should stop."""
330
+
331
+ if self.fitness is None or not type(self.fitness) is list:
332
+ self.arx = []
333
+ self.fitness = []
334
+ self.fitness.append(y)
335
+ self.arx.append(x)
336
+ if len(self.fitness) >= self.popsize:
337
+ self.fitness = np.asarray(self.fitness)
338
+ self.arx = np.array([self.fitfun.encode(x) for x in self.arx])
339
+ try:
340
+ self.arz = (linalg.inv(self.BD) @ \
341
+ ((self.arx - self.xmean).transpose() / self.sigma)).transpose()
342
+ except Exception:
343
+ if self.arz is None:
344
+ self.arz = self.randn(self.popsize, self.dim)
345
+ self.iterations += 1
346
+ self.updateCMA()
347
+ self.arz = None
348
+ self.arx = []
349
+ self.fitness = []
350
+
351
+ if is_debug_active():
352
+ self.n_evals.value += 1
353
+ if y < self.best_y.value or self.n_evals.value % 1000 == 999:
354
+ if y < self.best_y.value: self.best_y.value = y
355
+ t = time() - self.time_0
356
+ c = self.n_evals.value
357
+ message = '"c/t={0:.2f} c={1:d} t={2:.2f} y={3:.5f} yb={4:.5f} x={5!s}'.format(
358
+ c/t, c, t, y, self.best_y.value, x)
359
+ logger.debug(message)
360
+ return self.stop
361
+
268
362
  def newArgs(self):
269
- self.xmean = self.fitfun.closestFeasible(self.xmean)
270
- self.fitness = np.full(self.popsize, math.inf)
271
363
  # generate random offspring
272
364
  self.arz = self.randn(self.popsize, self.dim)
273
365
  delta = (self.BD @ self.arz.transpose()) * self.sigma
274
366
  self.arx = self.fitfun.closestFeasible(self.xmean + delta.transpose())
275
-
367
+
368
+ def do_optimize_delayed_update(self, fun, max_evals=None, workers=mp.cpu_count()):
369
+ if not max_evals is None:
370
+ self.max_evaluations = max_evals
371
+ evaluator = Evaluator(fun)
372
+ evaluator.start(workers)
373
+ evals_x = {}
374
+ self.evals = 0;
375
+ for _ in range(workers): # fill queue
376
+ x = self.ask_one()
377
+ evaluator.pipe[0].send((self.evals, x))
378
+ evals_x[self.evals] = x # store x
379
+ self.evals += 1
380
+
381
+ while True: # read from pipe, tell es and create new x
382
+ evals, y = evaluator.pipe[0].recv()
383
+
384
+ x = evals_x[evals] # retrieve evaluated x
385
+ del evals_x[evals]
386
+ stop = self.tell_one(y, x) # tell evaluated x
387
+ if stop != 0 or self.evals >= self.max_evaluations:
388
+ break # shutdown worker if stop criteria met
389
+
390
+ x = self.ask_one() # create new x
391
+ evaluator.pipe[0].send((self.evals, x))
392
+ evals_x[self.evals] = x # store x
393
+ self.evals += 1
394
+ evaluator.stop()
395
+ return self.best_x, self.best_value, evals, self.iterations, self.stop
396
+
276
397
  def doOptimize(self):
277
398
  # -------------------- Generation Loop --------------------------------
278
399
  while True:
@@ -281,10 +402,9 @@ class Cmaes(object):
281
402
  self.iterations += 1
282
403
  if self.fitfun.evaluation_counter > self.max_evaluations:
283
404
  break
284
- # Generate and evaluate popsize offspring
285
- self.newArgs()
286
- self.fitness = self.fitfun.values(self.arx)
287
- self.updateCMA()
405
+ xs = self.ask()
406
+ ys = self.fitfun.values(xs)
407
+ self.tell(ys, xs)
288
408
  if self.stop != 0:
289
409
  break
290
410
  return self.best_x, self.best_value, self.fitfun.evaluation_counter, self.iterations, self.stop
@@ -300,6 +420,9 @@ class Cmaes(object):
300
420
  if self.best_value > best_fitness:
301
421
  self.best_value = best_fitness
302
422
  self.best_x = self.fitfun.decode(self.arx[arindex[0]])
423
+ if best_fitness < self.stop_fitness:
424
+ self.stop = 1
425
+ return
303
426
 
304
427
  # Calculate new xmean, this is selection and recombination
305
428
  xold = self.xmean # for speed up of Eq. (2) and (3)
@@ -309,30 +432,29 @@ class Cmaes(object):
309
432
  bestArz = self.arz[bestIndex]
310
433
  zmean = np.transpose(bestArz) @ self.weights
311
434
  hsig = self.updateEvolutionPaths(zmean, xold)
312
- negccov = self.updateCovariance(hsig, bestArx, self.arz, arindex, xold)
313
- self.updateBD(negccov)
314
435
  # Adapt step size sigma - Eq. (5)
315
436
  self.sigma *= math.exp(min(1.0, (self.normps / self.chiN - 1.) * self.cs / self.damps))
316
- # handle termination criteria
317
- if self.stop_fitness != None: # only if stop_fitness is defined
318
- if best_fitness < self.stop_fitness:
319
- self.stop = 1
320
- return
321
- sqrtDiagC = np.sqrt(np.abs(self.diagC))
322
- pcCol = self.pc
323
- for i in range(self.dim):
324
- if self.sigma * max(abs(pcCol[i]), sqrtDiagC[i]) > self.stopTolX:
325
- break
326
- if i == self.dim - 1:
327
- self.stop = 2
328
- if self.stop != 0:
329
- return
330
- for i in range(self.dim):
331
- if self.sigma * sqrtDiagC[i] > self.stopTolUpX:
332
- self.stop = 3
333
- break
334
- if self.stop != 0:
335
- return
437
+
438
+ if self.iterations >= self.last_update + self.lazy_update_gap:
439
+ self.last_update = self.iterations
440
+ negccov = self.updateCovariance(hsig, bestArx, self.arz, arindex, xold)
441
+ self.updateBD(negccov)
442
+ # handle termination criteria
443
+ sqrtDiagC = np.sqrt(np.abs(self.diagC))
444
+ pcCol = self.pc
445
+ for i in range(self.dim):
446
+ if self.sigma * max(abs(pcCol[i]), sqrtDiagC[i]) > self.stopTolX:
447
+ break
448
+ if i == self.dim - 1:
449
+ self.stop = 2
450
+ if self.stop != 0:
451
+ return
452
+ for i in range(self.dim):
453
+ if self.sigma * sqrtDiagC[i] > self.stopTolUpX:
454
+ self.stop = 3
455
+ break
456
+ if self.stop != 0:
457
+ return
336
458
  history_best = min(self.fitness_history)
337
459
  history_worst = max(self.fitness_history)
338
460
  if self.iterations > 2 and max(history_worst, worstFitness) - min(history_best, best_fitness) < self.stopTolFun:
@@ -472,95 +594,4 @@ class Cmaes(object):
472
594
  self.diagD = np.sqrt(self.diagD) # diagD contains standard deviations now
473
595
 
474
596
  self.BD = self.B * self.diagD # O(n^2)
475
-
476
- def serial(fun):
477
- """Convert an objective function for serial execution for cmaes.minimize.
478
-
479
- Parameters
480
- ----------
481
- fun : objective function mapping a list of float arguments to a float value
482
-
483
- Returns
484
- -------
485
- out : function
486
- A function mapping a list of lists of float arguments to a list of float values
487
- by applying the input function in a loop."""
488
-
489
- return lambda xs : [_tryfun(fun, x) for x in xs]
490
-
491
- class parallel(object):
492
- """Convert an objective function for parallel execution for cmaes.minimize.
493
-
494
- Parameters
495
- ----------
496
- fun : objective function mapping a list of float arguments to a float value.
497
-
498
- represents a function mapping a list of lists of float arguments to a list of float values
499
- by applying the input function using parallel processes. stop needs to be called to avoid
500
- a resource leak"""
501
-
502
- def __init__(self, fun, workers):
503
- self.evaluator = Evaluator(fun)
504
- self.evaluator.start(workers)
505
-
506
- def __call__(self, xs):
507
- return eval_parallel(xs, self.evaluator)
508
-
509
- def stop(self):
510
- self.evaluator.stop()
511
-
512
- def _func_serial(fun, num, pid, xs, ys):
513
- for i in range(pid, len(xs), num):
514
- ys[i] = _tryfun(fun, xs[i])
515
-
516
- def _tryfun(fun, x):
517
- try:
518
- fit = fun(x)
519
- return fit if math.isfinite(fit) else sys.float_info.max
520
- except Exception:
521
- return sys.float_info.max
522
-
523
- def _check_bounds(bounds, guess, rg):
524
- if bounds is None and guess is None:
525
- raise ValueError('either guess or bounds need to be defined')
526
- if bounds is None:
527
- return None, None, np.asarray(guess)
528
- if guess is None:
529
- guess = rg.uniform(bounds.lb, bounds.ub)
530
- return np.asarray(bounds.lb), np.asarray(bounds.ub), np.asarray(guess)
531
-
532
- class _Fittness(object):
533
- """wrapper around the objective function, scales relative to boundaries."""
534
-
535
- def __init__(self, fun, lower, upper):
536
- self.fun = fun
537
- self.evaluation_counter = 0
538
- self.lower = lower
539
- if not lower is None:
540
- self.upper = upper
541
- self.scale = 0.5 * (upper - lower)
542
- self.typx = 0.5 * (upper + lower)
543
-
544
- def values(self, Xs): #enables parallel evaluation
545
- values = self.fun([self.decode(X) for X in Xs])
546
- self.evaluation_counter += len(Xs)
547
- return np.array(values)
548
-
549
- def closestFeasible(self, X):
550
- if self.lower is None:
551
- return X
552
- else:
553
- return np.maximum(np.minimum(X, 1.0), -1.0)
554
-
555
- def encode(self, X):
556
- if self.lower is None:
557
- return X
558
- else:
559
- return (X - self.typx) / self.scale
560
-
561
- def decode(self, X):
562
- if self.lower is None:
563
- return X
564
- else:
565
- return (X * self.scale) + self.typx
566
-
597
+