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/retry.py CHANGED
@@ -2,48 +2,52 @@
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
- import random
7
7
  import time
8
8
  import math
9
9
  import os
10
10
  import sys
11
+ import threadpoolctl
11
12
  import ctypes as ct
13
+ from scipy import interpolate
12
14
  import numpy as np
13
- from numpy.random import Generator, MT19937, SeedSequence
15
+ from numpy.random import Generator, PCG64DXSM, SeedSequence
14
16
  from scipy.optimize._constraints import new_bounds_to_old
15
17
  from scipy.optimize import OptimizeResult, Bounds
16
18
  import multiprocessing as mp
17
19
  from multiprocessing import Process
18
- from fcmaes.optimizer import de_cma, dtime, logger
20
+ from fcmaes.optimizer import de_cma, dtime, Optimizer
21
+ from fcmaes.evaluator import is_debug_active, is_trace_active
22
+ from loguru import logger
23
+ from typing import Optional, Callable, List
24
+ from numpy.typing import ArrayLike
19
25
 
20
26
  os.environ['MKL_DEBUG_CPU_TYPE'] = '5'
21
27
  os.environ['MKL_NUM_THREADS'] = '1'
22
28
  os.environ['OPENBLAS_NUM_THREADS'] = '1'
23
29
 
24
- def minimize(fun,
25
- bounds = None,
26
- value_limit = math.inf,
27
- num_retries = 1000,
28
- logger = None,
29
- workers = mp.cpu_count(),
30
- popsize = 31,
31
- max_evaluations = 50000,
32
- capacity = 500,
33
- stop_fittness = None,
34
- optimizer = None,
35
- ):
30
+ def minimize(fun: Callable[[ArrayLike], float],
31
+ bounds: Bounds,
32
+ value_limit: Optional[float] = np.inf,
33
+ num_retries: Optional[int] = 1024,
34
+ workers: Optional[int] = mp.cpu_count(),
35
+ popsize: Optional[int] = 31,
36
+ max_evaluations: Optional[int] = 50000,
37
+ capacity: Optional[int] = 500,
38
+ stop_fitness: Optional[float] = -np.inf,
39
+ optimizer: Optional[Optimizer] = None,
40
+ statistic_num: Optional[int] = 0,
41
+ ) -> OptimizeResult:
36
42
  """Minimization of a scalar function of one or more variables using parallel
37
- CMA-ES retry.
43
+ optimization retry.
38
44
 
39
45
  Parameters
40
46
  ----------
41
47
  fun : callable
42
48
  The objective function to be minimized.
43
- ``fun(x, *args) -> float``
44
- where ``x`` is an 1-D array with shape (n,) and ``args``
45
- is a tuple of the fixed parameters needed to completely
46
- specify the function.
49
+ ``fun(x) -> float``
50
+ where ``x`` is an 1-D array with shape (n,)
47
51
  bounds : sequence or `Bounds`, optional
48
52
  Bounds on variables. There are two ways to specify the bounds:
49
53
  1. Instance of the `scipy.Bounds` class.
@@ -53,10 +57,6 @@ def minimize(fun,
53
57
  Upper limit for optimized function values to be stored.
54
58
  num_retries : int, optional
55
59
  Number of optimization retries.
56
- logger : logger, optional
57
- logger for log output of the retry mechanism. If None, logging
58
- is switched off. Default is a logger which logs both to stdout and
59
- appends to a file ``optimizer.log``.
60
60
  workers : int, optional
61
61
  number of parallel processes used. Default is mp.cpu_count()
62
62
  popsize = int, optional
@@ -69,10 +69,12 @@ def minimize(fun,
69
69
  this setting is defined in the optimizer.
70
70
  capacity : int, optional
71
71
  capacity of the evaluation store.
72
- stop_fittness : float, optional
72
+ stop_fitness : float, optional
73
73
  Limit for fitness value. optimization runs terminate if this value is reached.
74
74
  optimizer : optimizer.Optimizer, optional
75
75
  optimizer to use. Default is a sequence of differential evolution and CMA-ES.
76
+ statistic_num: int, optional
77
+ if > 0 stores the progress of the optimization. Defines the size of this store.
76
78
 
77
79
  Returns
78
80
  -------
@@ -83,33 +85,151 @@ def minimize(fun,
83
85
  ``success`` a Boolean flag indicating if the optimizer exited successfully. """
84
86
 
85
87
  if optimizer is None:
86
- optimizer = de_cma(max_evaluations, popsize, stop_fittness)
87
- store = Store(bounds, capacity = capacity, logger = logger)
88
- return retry(fun, store, optimizer.minimize, num_retries, value_limit, workers)
89
-
90
- def retry(fun, store, optimize, num_retries, value_limit = math.inf, workers=mp.cpu_count()):
88
+ optimizer = de_cma(max_evaluations, popsize, stop_fitness)
89
+ store = Store(fun, bounds, capacity = capacity, statistic_num = statistic_num)
90
+ return retry(store, optimizer.minimize, num_retries, value_limit, workers, stop_fitness)
91
+
92
+ def retry(store: Store,
93
+ optimize: Callable,
94
+ num_retries: int,
95
+ value_limit: Optional[float] = np.inf,
96
+ workers: Optional[int] = mp.cpu_count(),
97
+ stop_fitness: Optional[float] = -np.inf) -> OptimizeResult:
98
+
91
99
  sg = SeedSequence()
92
- rgs = [Generator(MT19937(s)) for s in sg.spawn(workers)]
100
+ rgs = [Generator(PCG64DXSM(s)) for s in sg.spawn(workers)]
93
101
  proc=[Process(target=_retry_loop,
94
- args=(pid, rgs, fun, store, optimize, num_retries, value_limit)) for pid in range(workers)]
102
+ args=(pid, rgs, store, optimize, num_retries, value_limit, stop_fitness)) for pid in range(workers)]
95
103
  [p.start() for p in proc]
96
104
  [p.join() for p in proc]
97
105
  store.sort()
98
106
  store.dump()
99
107
  return OptimizeResult(x=store.get_x_best(), fun=store.get_y_best(),
100
108
  nfev=store.get_count_evals(), success=True)
109
+
110
+ def minimize_plot(name: str,
111
+ optimizer: Optimizer,
112
+ fun: Callable[[ArrayLike], float],
113
+ bounds: Bounds,
114
+ value_limit: Optional[float] = np.inf,
115
+ plot_limit: Optional[float] = np.inf,
116
+ num_retries: Optional[int] = 1024,
117
+ workers: Optional[int] = mp.cpu_count(),
118
+ stop_fitness: Optional[float] = -np.inf,
119
+ statistic_num: Optional[int] = 5000) -> OptimizeResult:
120
+
121
+ time0 = time.perf_counter() # optimization start time
122
+ name += '_' + optimizer.name
123
+ logger.info('optimize ' + name)
124
+ store = Store(fun, bounds, capacity = 500, statistic_num = statistic_num)
125
+ ret = retry(store, optimizer.minimize, num_retries, value_limit, workers, stop_fitness)
126
+ impr = store.get_improvements()
127
+ np.savez_compressed(name, ys=impr)
128
+ for _ in range(10):
129
+ filtered = np.array([imp for imp in impr if imp[1] < plot_limit])
130
+ if len(filtered) > 0:
131
+ impr = filtered
132
+ break
133
+ else:
134
+ plot_limit *= 3
135
+ logger.info(name + ' time ' + str(dtime(time0)))
136
+ plot(impr, 'progress_ret.' + name + '.png', label = name,
137
+ xlabel = 'time in sec', ylabel = r'$f$')
138
+ return ret
139
+
140
+ def plot(front: ArrayLike, fname: str, interp: Optional[bool] = True,
141
+ label: Optional[str] = r'$\chi$',
142
+ xlabel: Optional[str] = r'$f_1$', ylabel:Optional[str] = r'$f_2$',
143
+ zlabel: Optional[str] = r'$f_3$', plot3d: Optional[bool] = False,
144
+ s = 1, dpi=300):
145
+ if len(front[0]) == 3 and plot3d:
146
+ plot3(front, fname, label, xlabel, ylabel, zlabel)
147
+ return
148
+ if len(front[0]) >= 3:
149
+ for i in range(1, len(front[0])):
150
+ plot(front.T[np.array([0,i])].T, str(i) + '_' + fname,
151
+ interp=interp, ylabel = r'$f_{0}$'.format(i+1))
152
+ return
153
+ if len(front[0]) == 1:
154
+ ys = np.array(list(zip(range(100), [front[0][0]]*100)))
155
+ plot(ys, str(1) + '_' + fname,
156
+ interp=interp, xlabel = '', ylabel = r'$f_{0}$'.format(1))
157
+ return
158
+ import matplotlib.pyplot as pl
159
+ fig, ax = pl.subplots(1, 1)
160
+ x = front[:, 0]; y = front[:, 1]
161
+ if interp and len(x) > 2:
162
+ xa = np.argsort(x)
163
+ xs = x[xa]; ys = y[xa]
164
+ x = []; y = []
165
+ for i in range(len(xs)): # filter equal x values
166
+ if i == 0 or xs[i] > xs[i-1] + 1E-5:
167
+ x.append(xs[i]); y.append(ys[i])
168
+ tck = interpolate.InterpolatedUnivariateSpline(x,y,k=1)
169
+ x = np.linspace(min(x),max(x),1000)
170
+ y = [tck(xi) for xi in x]
171
+ ax.scatter(x, y, label=label, s=s)
172
+ ax.grid()
173
+ ax.set_xlabel(xlabel)
174
+ ax.set_ylabel(ylabel)
175
+ ax.legend()
176
+ fig.savefig(fname, dpi=dpi)
177
+ pl.close('all')
178
+
179
+ def plot3(front: ArrayLike, fname: str, label: Optional[str] =r'$\chi$',
180
+ xlabel: Optional[str] = r'$f_1$', ylabel: Optional[str] = r'$f_2$',
181
+ zlabel: Optional[str] = r'$f_3$'):
182
+ import matplotlib.pyplot as pl
183
+ fig = pl.figure()
184
+ ax = fig.add_subplot(projection='3d')
185
+ x = front[:, 0]; y = front[:, 1]; z = front[:, 2]
186
+ ax.scatter(x, y, z, label=label, s=1)
187
+ ax.grid()
188
+ ax.set_xlabel(xlabel)
189
+ ax.set_ylabel(ylabel)
190
+ ax.set_zlabel(zlabel)
191
+ ax.legend()
192
+ #pl.show()
193
+ fig.savefig(fname, dpi=300)
194
+ pl.close('all')
195
+
196
+
197
+ dtype_map = {
198
+ 'int32': ct.c_int32,
199
+ 'int64': ct.c_int64,
200
+ 'float32': ct.c_float,
201
+ 'float64': ct.c_double,
202
+ }
203
+
204
+ class Shared2d():
205
+
206
+ def __init__(self, xs):
207
+ self.rows, self.cols = xs.shape
208
+ self.dtype = xs.dtype
209
+ self.ra = mp.RawArray(dtype_map[str(xs.dtype)], self.rows*self.cols)
210
+ self.set(xs)
211
+
212
+ def set_i(self, i, x):
213
+ self.view()[i, :] = x
214
+
215
+ def view(self):
216
+ return np.frombuffer(self.ra, dtype=self.dtype).reshape((self.rows, self.cols))
217
+
218
+ def set(self, xs):
219
+ np.copyto(self.view(), xs)
101
220
 
102
221
  class Store(object):
103
222
  """thread safe storage for optimization retry results."""
104
223
 
105
224
  def __init__(self,
106
- bounds, # bounds of the objective function arguments
107
- check_interval = 10, # sort evaluation memory after check_interval iterations
108
- capacity = 500, # capacity of the evaluation store
109
- logger = None # if None logging is switched off
225
+ fun: Callable[[ArrayLike], float], # fitness function
226
+ bounds: Bounds, # bounds of the objective function arguments
227
+ check_interval: Optional[int] = 10, # sort evaluation memory after check_interval iterations
228
+ capacity: Optional[int] = 500, # capacity of the evaluation store
229
+ statistic_num: Optional[int] = 0
110
230
  ):
231
+ self.fun = fun
111
232
  self.lower, self.upper = _convertBounds(bounds)
112
- self.logger = logger
113
233
  self.capacity = capacity
114
234
  self.check_interval = check_interval
115
235
  self.dim = len(self.lower)
@@ -119,131 +239,142 @@ class Store(object):
119
239
 
120
240
  #shared between processes
121
241
  self.add_mutex = mp.Lock()
122
- self.xs = mp.RawArray(ct.c_double, self.capacity * self.dim)
242
+ self.xs = Shared2d(np.empty((self.capacity, self.dim), dtype = np.float64))
243
+ self.create_xs_view()
123
244
  self.ys = mp.RawArray(ct.c_double, self.capacity)
124
245
  self.count_evals = mp.RawValue(ct.c_long, 0)
125
246
  self.count_runs = mp.RawValue(ct.c_int, 0)
126
- self.num_stored = mp.RawValue(ct.c_int, 0)
127
- self.num_sorted = mp.RawValue(ct.c_int, 0)
247
+ self.num_stored = mp.RawValue(ct.c_int, 0)
128
248
  self.count_stat_runs = mp.RawValue(ct.c_int, 0)
129
249
  self.t0 = time.perf_counter()
130
250
  self.mean = mp.RawValue(ct.c_double, 0)
131
251
  self.qmean = mp.RawValue(ct.c_double, 0)
132
- self.best_y = mp.RawValue(ct.c_double, math.inf)
252
+ self.best_y = mp.RawValue(ct.c_double, np.inf)
133
253
  self.best_x = mp.RawArray(ct.c_double, self.dim)
134
- # statistics
135
- self.statistic_num = 1000
136
- self.time = mp.RawArray(ct.c_double, self.statistic_num)
137
- self.val = mp.RawArray(ct.c_double, self.statistic_num)
138
- self.si = mp.RawValue(ct.c_int, 0)
254
+ self.statistic_num = statistic_num
255
+ # statistics
256
+ self.statistic_num = statistic_num
257
+ if statistic_num > 0: # enable statistics
258
+ self.time = mp.RawArray(ct.c_double, self.statistic_num)
259
+ self.val = mp.RawArray(ct.c_double, self.statistic_num)
260
+ self.si = mp.RawValue(ct.c_int, 0)
261
+ self.sevals = mp.RawValue(ct.c_long, 0)
262
+ self.bval = mp.RawValue(ct.c_double, np.inf)
139
263
 
140
- # store improvement - time and value
141
- def add_statistics(self):
142
- si = self.si.value
143
- if si < self.statistic_num - 1:
144
- self.si.value = si + 1
145
- self.time[si] = dtime(self.t0)
146
- self.val[si] = self.best_y.value
147
-
264
+ # register improvement - time and value
265
+ def wrapper(self, x: ArrayLike):
266
+ y = self.fun(x)
267
+ self.sevals.value += 1
268
+ if y < self.bval.value:
269
+ self.bval.value = y
270
+ si = self.si.value
271
+ if si < self.statistic_num - 1:
272
+ self.si.value = si + 1
273
+ self.time[si] = dtime(self.t0)
274
+ self.val[si] = y
275
+ logger.trace(str(self.time[si]) + ' ' +
276
+ str(self.sevals.value) + ' ' +
277
+ str(int(self.sevals.value / self.time[si])) + ' ' +
278
+ str(y) + ' ' +
279
+ str(list(x)))
280
+ return y
281
+
148
282
  def get_improvements(self):
149
- return zip(self.time[:self.si.value], self.val[:self.si.value])
150
-
283
+ return np.array(list(zip(self.time[:self.si.value], self.val[:self.si.value])))
284
+
151
285
  # get num best values at evenly distributed times
152
- def get_statistics(self, num):
286
+ def get_statistics(self, num: int) -> List:
153
287
  ts = self.time[:self.si.value]
154
- vs = self.val[:self.si.value]
288
+ ys = self.val[:self.si.value]
155
289
  mt = ts[-1]
156
290
  dt = 0.9999999 * mt / num
157
291
  conv = []
158
292
  ti = 0
159
- val = vs[0]
293
+ val = ys[0]
160
294
  for i in range(num):
161
295
  while ts[ti] < (i+1) * dt:
162
296
  ti += 1
163
- val = vs[ti]
297
+ val = ys[ti]
164
298
  conv.append(val)
165
299
  return conv
166
300
 
167
- def eval_num(self, max_evals):
301
+ def eval_num(self, max_evals: int) -> int:
168
302
  return max_evals
169
303
 
170
- def replace(self, i, y, xs):
304
+ def replace(self, i: int, y: float, xs: ArrayLike):
171
305
  self.set_y(i, y)
172
306
  self.set_x(i, xs)
173
307
 
174
- def sort(self): # sort all entries to make room for new ones, determine best and worst
308
+ def sort(self) -> int: # sort all entries to make room for new ones, determine best and worst
175
309
  """sorts all store entries, keep only the 90% best to make room for new ones."""
176
310
  ns = self.num_stored.value
177
311
  ys = np.asarray(self.ys[:ns])
178
312
  yi = ys.argsort()
179
- sortRuns = []
180
- for i in range(len(yi)):
181
- y = ys[yi[i]]
182
- xs = self.get_x(yi[i])
183
- sortRuns.append((y, xs))
184
- numStored = min(len(sortRuns),int(0.9*self.capacity)) # keep 90% best
185
- for i in range(numStored):
186
- self.replace(i, sortRuns[i][0], sortRuns[i][1])
187
- self.num_sorted.value = numStored
313
+ numStored = min(ns, int(0.9*self.capacity)) # keep 90% best
314
+ self.xs_view[:numStored] = self.xs_view[yi][:numStored]
315
+ self.ys[:numStored] = ys[yi][:numStored]
188
316
  self.num_stored.value = numStored
189
317
  return numStored
190
318
 
191
- def add_result(self, y, xs, evals, limit=math.inf):
192
- """registers an optimization result at the score."""
319
+ def add_result(self, y: float, x: ArrayLike, evals: int, limit=np.inf):
320
+ """registers an optimization result at the store."""
193
321
  with self.add_mutex:
194
322
  self.incr_count_evals(evals)
195
323
  if y < limit:
196
324
  self.count_stat_runs.value += 1
197
325
  if y < self.best_y.value:
198
326
  self.best_y.value = y
199
- self.best_x[:] = xs[:]
200
- self.add_statistics()
327
+ self.best_x[:] = x[:]
201
328
  self.dump()
202
329
  if self.num_stored.value >= self.capacity-1:
203
330
  self.sort()
204
331
  cnt = self.count_stat_runs.value
205
- diff = y - self.mean.value
206
- self.qmean.value += (cnt - 1) * diff*diff / cnt;
332
+ diff = min(1E20, y - self.mean.value) # avoid overflow
333
+ self.qmean.value += (cnt - 1)/ cnt * diff*diff ;
207
334
  self.mean.value += diff / cnt
208
335
  ns = self.num_stored.value
209
336
  self.num_stored.value = ns + 1
210
- self.replace(ns, y, xs)
211
-
212
- def get_x(self, pid):
213
- return self.xs[pid*self.dim:(pid+1)*self.dim]
337
+ self.xs_view[self.num_stored.value, :] = x
338
+ self.ys[self.num_stored.value] = y
339
+ if is_debug_active():
340
+ dt = dtime(self.t0)
341
+ message = '{0} {1} {2} {3} {4:.6f} {5:.6f} {6:.2f} {7:.2f}'.format(
342
+ dt, int(self.count_evals.value / dt), self.count_runs.value, self.count_evals.value, \
343
+ y, self.best_y.value, self.get_y_mean(), self.get_y_standard_dev())
344
+ logger.debug(message)
214
345
 
215
- def get_x_best(self):
216
- return self.best_x[:]
346
+ def get_x_best(self) -> np.ndarray:
347
+ return np.array(self.best_x[:])
217
348
 
218
- def get_y(self, pid):
349
+ def create_xs_view(self): # needs to be called in the target process
350
+ self.xs_view = self.xs.view()
351
+
352
+ def get_xs(self) -> np.ndarray:
353
+ return self.xs.view()[:self.num_stored.value]
354
+
355
+ def get_y(self, pid: int) -> float:
219
356
  return self.ys[pid]
220
357
 
221
- def get_y_best(self):
358
+ def get_y_best(self) -> float:
222
359
  return self.best_y.value
223
360
 
224
- def get_ys(self):
225
- return self.ys[:self.num_stored.value]
361
+ def get_ys(self) -> np.ndarray:
362
+ return np.array(self.ys[:self.num_stored.value])
226
363
 
227
- def get_y_mean(self):
364
+ def get_y_mean(self) -> float:
228
365
  return self.mean.value
229
366
 
230
- def get_y_standard_dev(self):
231
- cnt = self.get_count_runs()
367
+ def get_y_standard_dev(self) -> float:
368
+ cnt = self.count_stat_runs.value
232
369
  return 0 if cnt <= 0 else math.sqrt(self.qmean.value / cnt)
233
370
 
234
- def get_count_evals(self):
371
+ def get_count_evals(self) -> int:
235
372
  return self.count_evals.value
236
373
 
237
- def get_count_runs(self):
374
+ def get_count_runs(self) -> int:
238
375
  return self.count_runs.value
239
-
240
- def set_x(self, pid, xs):
241
- self.xs[pid*self.dim:(pid+1)*self.dim] = xs[:]
242
-
243
- def set_y(self, pid, y):
244
- self.ys[pid] = y
245
-
246
- def get_runs_compare_incr(self, limit):
376
+
377
+ def get_runs_compare_incr(self, limit: float):
247
378
  with self.add_mutex:
248
379
  if self.count_runs.value < limit:
249
380
  self.count_runs.value += 1
@@ -258,36 +389,32 @@ class Store(object):
258
389
 
259
390
  def dump(self):
260
391
  """logs the current status of the store if logger defined."""
261
- if self.logger is None:
392
+ if not is_debug_active():
262
393
  return
263
394
  Ys = self.get_ys()
264
395
  vals = []
265
396
  for i in range(min(20, len(Ys))):
266
- vals.append(round(Ys[i],2))
267
- dt = dtime(self.t0)
268
-
397
+ vals.append(round(Ys[i],4))
398
+ dt = dtime(self.t0)
269
399
  message = '{0} {1} {2} {3} {4:.6f} {5:.2f} {6:.2f} {7!s} {8!s}'.format(
270
400
  dt, int(self.count_evals.value / dt), self.count_runs.value, self.count_evals.value, \
271
401
  self.best_y.value, self.get_y_mean(), self.get_y_standard_dev(), vals, self.best_x[:])
272
- self.logger.info(message)
402
+ logger.debug(message)
273
403
 
274
404
 
275
- def _retry_loop(pid, rgs, fun, store, optimize, num_retries, value_limit):
276
-
277
- #reinitialize logging config for windows - multi threading fix
278
- if 'win' in sys.platform and not store.logger is None:
279
- store.logger = logger()
280
-
405
+ def _retry_loop(pid, rgs, store, optimize, num_retries, value_limit, stop_fitness = -np.inf):
406
+ store.create_xs_view()
407
+ fun = store.wrapper if store.statistic_num > 0 else store.fun
281
408
  lower = store.lower
282
- while store.get_runs_compare_incr(num_retries):
283
- try:
284
- sol, y, evals = optimize(fun, Bounds(store.lower, store.upper), None,
285
- [random.uniform(0.05, 0.1)]*len(lower), rgs[pid], store)
286
- store.add_result(y, sol, evals, value_limit)
287
- except Exception as ex:
288
- continue
289
- # if pid == 0:
290
- # store.dump()
409
+ with threadpoolctl.threadpool_limits(limits=1, user_api="blas"):
410
+ while store.get_runs_compare_incr(num_retries) and store.best_y.value > stop_fitness:
411
+ try:
412
+ rg = rgs[pid]
413
+ sol, y, evals = optimize(fun, Bounds(store.lower, store.upper), None,
414
+ [rg.uniform(0.05, 0.1)]*len(lower), rg, store)
415
+ store.add_result(y, sol, evals, value_limit)
416
+ except Exception as ex:
417
+ print(str(ex))
291
418
 
292
419
  def _convertBounds(bounds):
293
420
  if bounds is None: