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/diversifier.py ADDED
@@ -0,0 +1,357 @@
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 an diversifying wrapper / parallel retry mechanism.
8
+
9
+ Uses the archive from CVT MAP-Elites (https://arxiv.org/abs/1610.05729)
10
+ and generalizes ideas from CMA-ME (https://arxiv.org/pdf/1912.02400.pdf)
11
+ to other wrapped algorithms.
12
+
13
+ Both the parallel retry and the archive based modification of the fitness
14
+ function enhance the diversification of the optimization result.
15
+ The resulting archive may be stored and can be used to continue the
16
+ optimization later.
17
+
18
+ Requires a QD-fitness function returning both an fitness value and a
19
+ behavior vector used to determine the corresponding archive niche using
20
+ Voronoi tesselation.
21
+
22
+ Returns an archive of niche-elites containing also for each niche statistics
23
+ about the associated solutions.
24
+ """
25
+
26
+ import numpy as np
27
+ from numpy.random import Generator, PCG64DXSM, SeedSequence
28
+ from multiprocessing import Process
29
+ from scipy.optimize import Bounds
30
+ from fcmaes.optimizer import dtime, de_cma, Optimizer
31
+ import multiprocessing as mp
32
+ import ctypes as ct
33
+ from time import perf_counter
34
+ from fcmaes.mapelites import Archive, update_archive, rng
35
+ from fcmaes import advretry
36
+ from fcmaes.evaluator import is_debug_active
37
+ from loguru import logger
38
+ import threadpoolctl
39
+
40
+ from typing import Optional, Callable, Tuple, Dict
41
+ from numpy.typing import ArrayLike
42
+
43
+ def minimize(qd_fitness: Callable[[ArrayLike], Tuple[float, np.ndarray]],
44
+ bounds: Bounds,
45
+ qd_bounds: Bounds,
46
+ niche_num: Optional[int] = 10000,
47
+ samples_per_niche: Optional[int] = 20,
48
+ max_evals: Optional[int] = None,
49
+ workers: Optional[int] = mp.cpu_count(),
50
+ archive: Optional[Archive] = None,
51
+ opt_params: Optional[Dict] = {},
52
+ use_stats: Optional[bool] = False,
53
+ ) -> Archive:
54
+
55
+ """Wraps an fcmaes optmizer/solver by hijacking its tell function.
56
+ Works as CVT Map-Elites in maintaining an archive of diverse elites.
57
+ But this archive is not used to derive solution vectors, but to reevaluate them.
58
+ For each fitness result it determines its niche. The "told" fitness is
59
+ determined relative to its local elite. If it is better the evaluated solution
60
+ becomes the new niche-elite.
61
+ This way the wrapped solver is "tricked" to follow a QD-goal: Finding empty niches
62
+ and improving all niches. This works not only for CMA-ES, but also for other
63
+ solvers: DE, CR-FM-NES and PGPE. Both their Python and C++ versions are supported.
64
+
65
+ Parameters
66
+ ----------
67
+ solver : evolutionary algorithm, needs to support ask/tell
68
+ qd_fitness : callable
69
+ The objective function to be minimized. Returns a fitness value and a behavior vector.
70
+ ``qd_fitness(x) -> float, array``
71
+ where ``x`` is an 1-D array with shape (n,)
72
+ bounds : `Bounds`
73
+ Bounds on variables. Instance of the `scipy.Bounds` class.
74
+ qd_bounds : `Bounds`
75
+ Bounds on behavior descriptors. Instance of the `scipy.Bounds` class.
76
+ niche_num : int, optional
77
+ Number of niches.
78
+ samples_per_niche : int, optional
79
+ Number of samples used for niche computation.
80
+ If samples_per_niche > 0 cvt-clustering is used, else grid-clustering is used.
81
+ max_evals : int, optional
82
+ Number of fitness evaluations.
83
+ workers : int, optional
84
+ Number of spawned parallel worker processes.
85
+ archive : Archive, optional
86
+ If defined MAP-elites is continued for this archive.
87
+ opt_params : dictionary, optional (or a list/tuple/array of these)
88
+ Parameters selecting and configuring the wrapped solver.
89
+ 'solver' - supported are 'CMA','CMA_CPP','CRMFNES','CRMFNES_CPP','DE','DE_CPP','PGPE'
90
+ default is 'CMA_CPP'
91
+ 'popsize' - population size, default = 32
92
+ 'sigma' - initial distribution sigma, default = rg.uniform(0.03, 0.3)**2)
93
+ 'mean' - initial distribution mean, default=rg.uniform(bounds.lb, bounds.ub))
94
+ 'max_evals' - maximal number of evaluations per run, default = 50000
95
+ 'stall_criterion' - how many iterations without progress allowed, default = 50 iterations
96
+ If a list/tuple/array of parameters are given, the corresponding solvers are called in a
97
+ sequence.
98
+ use_stats : bool, optional
99
+ If True, archive accumulates statistics of the solutions
100
+
101
+ Returns
102
+ -------
103
+ archive : Archive
104
+ Resulting archive of niches. Can be stored for later continuation of MAP-elites."""
105
+
106
+ if max_evals is None:
107
+ max_evals = workers*50000
108
+ dim = len(bounds.lb)
109
+ if archive is None:
110
+ archive = Archive(dim, qd_bounds, niche_num, use_stats)
111
+ archive.init_niches(samples_per_niche)
112
+ # initialize archive with random values
113
+ archive.xs_view[:] = rng.uniform(bounds.lb, bounds.ub, (niche_num, dim))
114
+ t0 = perf_counter()
115
+ qd_fitness.archive = archive # attach archive for logging
116
+ minimize_parallel_(archive, qd_fitness, bounds, workers, opt_params, max_evals)
117
+ if is_debug_active():
118
+ ys = np.sort(archive.get_ys())[:min(100, archive.capacity)] # best fitness values
119
+ logger.debug(f'best {min(ys):.3f} worst {max(ys):.3f} ' +
120
+ f'mean {np.mean(ys):.3f} stdev {np.std(ys):.3f} time {dtime(t0)} s')
121
+ return archive
122
+
123
+ def apply_advretry(fitness: Callable[[ArrayLike], float],
124
+ qd_fitness: Callable[[ArrayLike], Tuple[float, np.ndarray]],
125
+ bounds: Bounds,
126
+ archive: Archive,
127
+ optimizer: Optional[Optimizer] = None,
128
+ num_retries: Optional[int] = 1000,
129
+ workers: Optional[int] = mp.cpu_count(),
130
+ max_eval_fac: Optional[float] = 5.0,
131
+ xs: Optional[np.ndarray] = None,
132
+ ys: Optional[np.ndarray] = None,
133
+ x_conv: Callable[[ArrayLike], ArrayLike] = None):
134
+
135
+ """Unifies the QD world with traditional optimization. It converts
136
+ a QD-archive into a multiprocessing store used by the fcmaes smart
137
+ boundary management meta algorithm (advretry). Then advretry is applied
138
+ to find the global optimum. Finally the updated store is feed back into
139
+ the QD-archive. For this we need a descriptor generating function
140
+ 'descriptors' which may require reevaluation of the new solutions.
141
+
142
+ Parameters
143
+ ----------
144
+ solver : evolutionary algorithm, needs to support ask/tell
145
+ fitness : callable
146
+ The objective function to be minimized. Returns a fitness value.
147
+ ``fitness(x) -> float``
148
+ qf_fun : callable
149
+ Generates the descriptors for a solution. Returns a behavior vector.
150
+ ``descriptors(x) -> array``
151
+ where ``x`` is an 1-D array with shape (n,)
152
+ bounds : `Bounds`
153
+ Bounds on variables. Instance of the `scipy.Bounds` class.
154
+ archive : Archive
155
+ Improves the solutions if this archive.
156
+ optimizer : optimizer.Optimizer, optional
157
+ Optimizer to use. Default is a sequence of differential evolution and CMA-ES.
158
+ num_retries : int, optional
159
+ Number of optimization runs.
160
+ workers : int, optional
161
+ Number of spawned parallel worker processes.
162
+ max_eval_fac : int, optional
163
+ Final limit of the number of function evaluations = max_eval_fac*min_evaluations
164
+ xs : ndarray, optional
165
+ Used to initialize advretry. If undefined the archive content is used.
166
+ If xs is defined, ys must be too
167
+ ys : ndarray, optional
168
+ Used to initialize advretry. If undefined the archive content is used.
169
+ x_conv : callable, optional
170
+ If defined converts the x in xs to solutions suitable for the given archive.
171
+ If undefined it is assumed that the x in xs are valid archive solutons.
172
+ """
173
+
174
+ if optimizer is None:
175
+ optimizer = de_cma(1500)
176
+ # generate advretry store
177
+ store = advretry.Store(fitness, bounds, num_retries=num_retries,
178
+ max_eval_fac=max_eval_fac)
179
+
180
+ # select only occupied entries
181
+ if xs is None:
182
+ ys = archive.get_ys()
183
+ valid = (ys < np.inf)
184
+ ys = ys[valid]
185
+ xs = archive.xs_view[valid]
186
+ t0 = perf_counter()
187
+ # transfer to advretry store
188
+ for i in range(len(ys)):
189
+ store.add_result(ys[i], xs[i], 1)
190
+ # perform parallel retry
191
+ advretry.retry(store, optimizer.minimize, workers=workers)
192
+ # transfer back to archive
193
+ xs = store.xs_view
194
+ if not x_conv is None:
195
+ xs = [x_conv(x) for x in xs]
196
+ yds = [qd_fitness(x) for x in xs]
197
+ descs = np.array([yd[1] for yd in yds])
198
+ ys = np.array([yd[0] for yd in yds])
199
+ niches = archive.index_of_niches(descs)
200
+ for i in range(len(ys)):
201
+ archive.set(niches[i], (ys[i], descs[i]), xs[i])
202
+ archive.argsort()
203
+ if is_debug_active():
204
+ ys = np.sort(archive.get_ys())[:min(100, archive.capacity)] # best fitness values
205
+ logger.debug(f'best {min(ys):.3f} worst {max(ys):.3f} ' +
206
+ f'mean {np.mean(ys):.3f} stdev {np.std(ys):.3f} time {dtime(t0)} s')
207
+
208
+ def minimize_parallel_(archive, fitness, bounds, workers, opt_params, max_evals):
209
+ sg = SeedSequence()
210
+ rgs = [Generator(PCG64DXSM(s)) for s in sg.spawn(workers)]
211
+ evals = mp.RawValue(ct.c_long, 0)
212
+ proc=[Process(target=run_minimize_,
213
+ args=(archive, fitness, bounds, rgs[p],
214
+ opt_params, p, workers, evals, max_evals)) for p in range(workers)]
215
+ [p.start() for p in proc]
216
+ [p.join() for p in proc]
217
+
218
+ def run_minimize_(archive, fitness, bounds, rg, opt_params, p, workers, evals, max_evals):
219
+ with threadpoolctl.threadpool_limits(limits=1, user_api="blas"):
220
+ if isinstance(opt_params, (list, tuple, np.ndarray)):
221
+ default_workers = int(workers/2) if len(opt_params) > 1 else workers
222
+ for params in opt_params: # call MAP-Elites
223
+ if 'elites' == params.get('solver'):
224
+ elites_workers = params.get('workers', default_workers)
225
+ if p < elites_workers:
226
+ run_map_elites_(archive, fitness, bounds, rg, evals, max_evals, params)
227
+ return
228
+ while evals.value < max_evals: # call solvers in loop
229
+ best_x = None
230
+ if isinstance(opt_params, (list, tuple, np.ndarray)):
231
+ for params in opt_params: # call in sequence
232
+ if 'elites' == params.get('solver'):
233
+ continue # ignore in loop
234
+ if best_x is None:
235
+ # selecting a niche elite is no improvement over random x0
236
+ x0 = None#, _, _ = archive.random_xs_one(select_n, rg)
237
+ best_x = minimize_(archive, fitness, bounds, rg, evals, max_evals, params,
238
+ x0 = x0)
239
+ else:
240
+ best_x = minimize_(archive, fitness, bounds, rg, evals, max_evals, params, x0 = best_x)
241
+ else:
242
+ minimize_(archive, fitness, bounds, rg, evals, max_evals, opt_params)
243
+
244
+ from fcmaes.mapelites import variation_, iso_dd_
245
+
246
+ def run_map_elites_(archive, fitness, bounds, rg, evals, max_evals, opt_params = {}):
247
+ popsize = opt_params.get('popsize', 32)
248
+ use_sbx = opt_params.get('use_sbx', True)
249
+ dis_c = opt_params.get('dis_c', 20)
250
+ dis_m = opt_params.get('dis_m', 20)
251
+ iso_sigma = opt_params.get('iso_sigma', 0.01)
252
+ line_sigma = opt_params.get('line_sigma', 0.2)
253
+ select_n = archive.capacity
254
+ while evals.value < max_evals:
255
+ if use_sbx:
256
+ pop = archive.random_xs(select_n, popsize, rg)
257
+ xs = variation_(pop, bounds.lb, bounds.ub, rg, dis_c, dis_m)
258
+ else:
259
+ x1 = archive.random_xs(select_n, popsize, rg)
260
+ x2 = archive.random_xs(select_n, popsize, rg)
261
+ xs = iso_dd_(x1, x2, bounds.lb, bounds.ub, rg, iso_sigma, line_sigma)
262
+ yds = [fitness(x) for x in xs]
263
+ evals.value += popsize
264
+ descs = np.array([yd[1] for yd in yds])
265
+ niches = archive.index_of_niches(descs)
266
+ for i in range(len(yds)):
267
+ archive.set(niches[i], yds[i], xs[i])
268
+ archive.argsort()
269
+ select_n = archive.get_occupied()
270
+
271
+ def minimize_(archive, fitness, bounds, rg, evals, max_evals, opt_params, x0 = None):
272
+ if 'BITE_CPP' == opt_params.get('solver'):
273
+ return run_bite_(archive, fitness, bounds, rg, evals, max_evals, opt_params, x0 = None)
274
+ else:
275
+ es = get_solver_(bounds, opt_params, rg, x0)
276
+ stall_criterion = opt_params.get('stall_criterion', 20)
277
+ max_evals_iter = opt_params.get('max_evals', 50000)
278
+ max_iters = int(max_evals_iter/es.popsize)
279
+ old_ys = None
280
+ last_improve = 0
281
+ best_x = None
282
+ best_y = np.inf
283
+ for iter in range(max_iters):
284
+ xs = es.ask()
285
+ ys, real_ys = update_archive(archive, xs, fitness)
286
+ evals.value += es.popsize
287
+ # update best real fitness
288
+ yi = np.argmin(real_ys)
289
+ ybest = real_ys[yi]
290
+ if ybest < best_y:
291
+ best_y = ybest
292
+ best_x = xs[yi]
293
+ if not old_ys is None:
294
+ if (np.sort(ys) < old_ys).any():
295
+ last_improve = iter
296
+ if last_improve + stall_criterion < iter:
297
+ break
298
+ stop = es.tell(ys)
299
+ if stop != 0 or evals.value >= max_evals:
300
+ break
301
+ old_ys = np.sort(ys)
302
+ return best_x # real best solution
303
+
304
+ from fcmaes import cmaes, cmaescpp, crfmnescpp, pgpecpp, decpp, crfmnes, de, bitecpp
305
+
306
+ def run_bite_(archive, fitness, bounds, rg, evals, max_evals, opt_params, x0 = None):
307
+ # BiteOpt doesn't support ask/tell, so we have to "patch" fitness. Note that Voronoi
308
+ # tesselation is more expensive if called for single behavior vectors and not for batches.
309
+
310
+ def fit(x: Callable[[ArrayLike], float]):
311
+ if evals.value >= max_evals:
312
+ return np.inf
313
+ evals.value += 1
314
+ ys, _ = update_archive(archive, [x], fitness)
315
+ return ys[0]
316
+
317
+ max_evals_iter = opt_params.get('max_evals', 50000)
318
+ stall_criterion = opt_params.get('stall_criterion', 20)
319
+ #popsize = opt_params.get('popsize', 0)
320
+ ret = bitecpp.minimize(fit, bounds, x0 = x0, M = 1,
321
+ stall_criterion = stall_criterion,
322
+ max_evaluations = max_evals_iter, rg = rg)
323
+ return ret.x
324
+
325
+ def get_solver_(bounds, opt_params, rg, x0 = None):
326
+ dim = len(bounds.lb)
327
+ popsize = opt_params.get('popsize', 31)
328
+ #sigma = opt_params.get('sigma',rg.uniform(0.03, 0.3)**2)
329
+ sigma = opt_params.get('sigma',rg.uniform(0.1, 0.5)**2)
330
+ #sigma = opt_params.get('sigma',rg.uniform(0.2, 0.5)**2)
331
+ #sigma = opt_params.get('sigma',rg.uniform(0.1, 0.5))
332
+ mean = opt_params.get('mean', rg.uniform(bounds.lb, bounds.ub)) \
333
+ if x0 is None else x0
334
+ name = opt_params.get('solver', 'CMA_CPP')
335
+ if name == 'CMA':
336
+ return cmaes.Cmaes(bounds, x0 = mean,
337
+ popsize = popsize, input_sigma = sigma, rg = rg)
338
+ elif name == 'CMA_CPP':
339
+ return cmaescpp.ACMA_C(dim, bounds, x0 = mean, #stop_hist = 0,
340
+ popsize = popsize, input_sigma = sigma, rg = rg)
341
+ elif name == 'CRMFNES':
342
+ return crfmnes.CRFMNES(dim, bounds, x0 = mean,
343
+ popsize = popsize, input_sigma = sigma, rg = rg)
344
+ elif name == 'CRMFNES_CPP':
345
+ return crfmnescpp.CRFMNES_C(dim, bounds, x0 = mean,
346
+ popsize = popsize, input_sigma = sigma, rg = rg)
347
+ elif name == 'DE':
348
+ return de.DE(dim, bounds, popsize = popsize, rg = rg)
349
+ elif name == 'DE_CPP':
350
+ return decpp.DE_C(dim, bounds, popsize = popsize, rg = rg)
351
+ elif name == 'PGPE':
352
+ return pgpecpp.PGPE_C(dim, bounds, x0 = mean,
353
+ popsize = popsize, input_sigma = sigma, rg = rg)
354
+ else:
355
+ print ("invalid solver")
356
+ return None
357
+