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/mapelites.py ADDED
@@ -0,0 +1,737 @@
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 CVT MAP-Elites including CMA-ES emitter and CMA-ES drilldown.
8
+
9
+ See https://arxiv.org/abs/1610.05729 and https://arxiv.org/pdf/1912.02400.pdf
10
+
11
+ MAP-Elites implementations differ in the following details:
12
+
13
+ 1) Initialisation of the behavior space:
14
+
15
+ a) Generated from some solution distribution by applying the fitness function to determine their behavior.
16
+ b) Generated from uniform samples of the behavior space.
17
+
18
+ We use b) because random solutions may cover only parts of the behavior space. Some parts may only be reachable
19
+ by optimization. Another reason: Fitness computations may be expensive. Therefore we don't compute fitness
20
+ values for the initial solution population.
21
+
22
+ 2) Initialization of the niches:
23
+
24
+ a) Generated from some solution distribution.
25
+ b) Generated from uniform samples of the solution space. These solutions are never evaluated but serve as
26
+ initial population for SBX or Iso+LineDD. Their associated fitness value is set to math.inf (infinity).
27
+
28
+ We use b) because this way we:
29
+ - Avoid computing fitness values for the initial population.
30
+ - Enhance the diversity of initial solutions emitted by SBX or Iso+LineDD.
31
+
32
+ 3) Iso+LineDD (https://arxiv.org/pdf/1804.03906) is implemented but doesn't work well with extremely ragged solution
33
+ landscapes. Therefore SBX+mutation is the default setting.
34
+
35
+ 4) SBX (Simulated binary crossover) is taken from mode.py and simplified. It is combined with mutation.
36
+ Both spread factors - for crossover and mutation - are randomized for each application.
37
+
38
+ 5) Candidates for CMA-ES are sampled with a bias to better niches. As for SBX only a subset of the archive is
39
+ used, the worst niches are ignored.
40
+
41
+ 6) There is a CMA-ES drill down for specific niches - in this mode all solutions outside the niche
42
+ are rejected. Restricted solution box bounds are used derived from statistics maintained by the archive
43
+ during the addition of new solution candidates.
44
+
45
+ 7) The QD-archive uses shared memory to reduce inter-process communication overhead.
46
+ """
47
+
48
+ import numpy as np
49
+ from numpy.random import Generator, PCG64DXSM, SeedSequence
50
+ from multiprocessing import Process
51
+ import multiprocessing as mp
52
+ from sklearn.neighbors import KDTree
53
+ from sklearn.cluster import KMeans
54
+ from scipy.optimize import Bounds
55
+ from pathlib import Path
56
+ from fcmaes.retry import Shared2d
57
+ from fcmaes.optimizer import dtime
58
+ from fcmaes import cmaescpp
59
+ from numpy.random import default_rng
60
+ import ctypes as ct
61
+ from time import perf_counter
62
+ import threadpoolctl
63
+ from numba import njit
64
+ from fcmaes.evaluator import is_debug_active
65
+ from loguru import logger
66
+
67
+ from typing import Optional, Callable, Tuple, Dict
68
+ from numpy.typing import ArrayLike
69
+
70
+ rng = default_rng()
71
+
72
+ def optimize_map_elites(qd_fitness: Callable[[ArrayLike], Tuple[float, np.ndarray]],
73
+ bounds: Bounds,
74
+ qd_bounds: Bounds,
75
+ niche_num: Optional[int] = 4000,
76
+ samples_per_niche: Optional[int] = 20,
77
+ workers: Optional[int] = mp.cpu_count(),
78
+ iterations: Optional[int] = 100,
79
+ archive: Optional[Archive] = None,
80
+ me_params: Optional[Dict] = {},
81
+ cma_params: Optional[Dict] = {},
82
+ use_stats: Optional[bool] = False,
83
+ ) -> Archive:
84
+
85
+ """Application of CVT-Map Elites with additional CMA-ES emmitter.
86
+
87
+ Parameters
88
+ ----------
89
+ qd_fitness : callable
90
+ The objective function to be minimized.
91
+ ``qd_fitness(x) -> float, array``
92
+ where ``x`` is an 1-D array with shape (n,)
93
+ bounds : `Bounds`
94
+ Bounds on variables. Instance of the `scipy.Bounds` class.
95
+ qd_bounds : `Bounds`
96
+ Bounds on behavior descriptors. Instance of the `scipy.Bounds` class.
97
+ niche_num : int, optional
98
+ Number of niches.
99
+ samples_per_niche : int, optional
100
+ Number of samples used for niche computation.
101
+ max_evaluations : int, optional
102
+ Forced termination after ``max_evaluations`` function evaluations.
103
+ workers : int, optional
104
+ Number of spawned parallel worker processes.
105
+ iterations : int, optional
106
+ Number of MAP-elites iterations.
107
+ archive : Archive, optional
108
+ If defined MAP-elites is continued for this archive.
109
+ me_params : dictionary, optional
110
+ Parameters for MAP-elites.
111
+ cma_params : dictionary, optional
112
+ Parameters for the CMA-ES emitter.
113
+ use_stats : bool, optional
114
+ If True, archive accumulates statistics of the solutions
115
+
116
+ Returns
117
+ -------
118
+ archive : Archive
119
+ Resulting archive of niches. Can be stored for later continuation of MAP-elites."""
120
+
121
+ dim = len(bounds.lb)
122
+ if archive is None:
123
+ archive = Archive(dim, qd_bounds, niche_num, use_stats)
124
+ archive.init_niches(samples_per_niche)
125
+ # initialize archive with random values
126
+ self.xs.view()[:] = rng.uniform(bounds.lb, bounds.ub, (niche_num, dim))
127
+ t0 = perf_counter()
128
+ qd_fitness.archive = archive # attach archive for logging
129
+ for iter in range(iterations):
130
+ archive.argsort() # sort archive to select the best_n
131
+ optimize_map_elites_(archive, qd_fitness, bounds, workers,
132
+ me_params, cma_params)
133
+ if is_debug_active():
134
+ ys = np.sort(archive.get_ys())[:100] # best 100 fitness values
135
+ logger.debug(f'best 100 iter {iter} best {min(ys):.3f} worst {max(ys):.3f} ' +
136
+ f'mean {np.mean(ys):.3f} stdev {np.std(ys):.3f} time {dtime(t0)} s')
137
+ return archive
138
+
139
+ def empty_archive(dim: int,
140
+ qd_bounds: Bounds,
141
+ niche_num: int,
142
+ samples_per_niche: int,
143
+ use_stats: Optional[bool] = False) -> Archive:
144
+
145
+ """Creates an empty archive.
146
+
147
+ Parameters
148
+ ----------
149
+ archive: Archive
150
+ qd_bounds : `Bounds`
151
+ Bounds on behavior descriptors. Instance of the `scipy.Bounds` class.
152
+ niche_num : int, optional
153
+ Number of niches.
154
+ samples_per_niche : int, optional
155
+ Number of samples used for niche computation.
156
+ use_stats : bool, optional
157
+ If True, archive accumulates statistics of the solutions
158
+
159
+ Returns
160
+ -------
161
+ archive : Archive
162
+ Empty archive of niches."""
163
+
164
+ archive = Archive(dim, qd_bounds, niche_num, use_stats)
165
+ archive.init_niches(samples_per_niche)
166
+ return archive
167
+
168
+ def set_KDTree(archive: Archive,
169
+ centers:Optional[np.ndarray] = None,
170
+ niche_num: Optional[int] = None,
171
+ qd_bounds: Optional[Bounds] = None,
172
+ samples_per_niche: Optional[int] = 100):
173
+
174
+ """Returns a function deciding niche membership.
175
+
176
+ Parameters
177
+ ----------
178
+ archive: Archive
179
+ centers : ndarray, shape (n,m), optional
180
+ If defined, these behavior vectors are used as niche centers
181
+ niche_num : int, optional
182
+ Number of niches. Required if centers is None.
183
+ qd_bounds : `Bounds`
184
+ Bounds on behavior descriptors. Instance of the `scipy.Bounds` class.
185
+ Required if centers is None.
186
+ samples_per_niche : int, optional
187
+ Number of samples used for niche computation.
188
+ If samples_per_niche > 0 cvt-clustering is used, else grid-clustering is used.
189
+
190
+ Returns
191
+ -------
192
+ index_of_niches : callable
193
+ Maps an array of description vectors to their corresponding niche indices.
194
+ centers : ndarray, shape (n,m)
195
+ behavior vectors used as niche centers."""
196
+
197
+ if centers is None: # cache centers
198
+ centers = get_centers_(niche_num, len(qd_bounds.lb), samples_per_niche)
199
+ archive.kdt = KDTree(centers, leaf_size=30, metric='euclidean')
200
+ archive.centers = centers
201
+
202
+ def load_archive(name: str,
203
+ bounds: Bounds,
204
+ qd_bounds: Bounds,
205
+ niche_num: Optional[int] = 10000,
206
+ use_stats: Optional[bool] = False,
207
+ ) -> Archive:
208
+
209
+ """Loads an archive from disk.
210
+
211
+ Parameters
212
+ ----------
213
+ name: string
214
+ Name of the archive.
215
+ bounds : `Bounds`
216
+ Bounds on variables. Instance of the `scipy.Bounds` class.
217
+ qd_bounds : `Bounds`
218
+ Bounds on behavior descriptors. Instance of the `scipy.Bounds` class.
219
+ niche_num : int, optional
220
+ Number of niches.
221
+ use_stats : bool, optional
222
+ If True, archive accumulates statistics of the solutions
223
+
224
+ Returns
225
+ -------
226
+ archive : Archive
227
+ Archive of niches. Can be used for continuation of MAP-elites."""
228
+
229
+ dim = len(bounds.lb)
230
+ archive = Archive(dim, qd_bounds, niche_num, name, use_stats)
231
+ archive.load(name)
232
+ return archive
233
+
234
+ def optimize_map_elites_(archive, fitness, bounds, workers,
235
+ me_params, cma_params):
236
+ sg = SeedSequence()
237
+ rgs = [Generator(PCG64DXSM(s)) for s in sg.spawn(workers)]
238
+ proc=[Process(target=run_map_elites_,
239
+ args=(archive, fitness, bounds, rgs[p],
240
+ me_params, cma_params)) for p in range(workers)]
241
+ [p.start() for p in proc]
242
+ [p.join() for p in proc]
243
+
244
+ def run_map_elites_(archive, fitness, bounds, rg,
245
+ me_params, cma_params):
246
+ generations = me_params.get('generations', 10)
247
+ chunk_size = me_params.get('chunk_size', 20)
248
+ use_sbx = me_params.get('use_sbx', True)
249
+ dis_c = me_params.get('dis_c', 20)
250
+ dis_m = me_params.get('dis_m', 20)
251
+ iso_sigma = me_params.get('iso_sigma', 0.02)
252
+ line_sigma = me_params.get('line_sigma', 0.2)
253
+ cma_generations = cma_params.get('cma_generations', 20)
254
+ select_n = archive.capacity
255
+ with threadpoolctl.threadpool_limits(limits=1, user_api="blas"):
256
+ for _ in range(generations):
257
+ if use_sbx:
258
+ pop = archive.random_xs(select_n, chunk_size, rg)
259
+ xs = variation_(pop, bounds.lb, bounds.ub, rg, dis_c, dis_m)
260
+ else:
261
+ x1 = archive.random_xs(select_n, chunk_size, rg)
262
+ x2 = archive.random_xs(select_n, chunk_size, rg)
263
+ xs = iso_dd_(x1, x2, bounds.lb, bounds.ub, rg, iso_sigma, line_sigma)
264
+ yds = [fitness(x) for x in xs]
265
+ descs = np.array([yd[1] for yd in yds])
266
+ niches = archive.index_of_niches(descs)
267
+ for i in range(len(yds)):
268
+ archive.set(niches[i], yds[i], xs[i])
269
+ archive.argsort()
270
+ select_n = archive.get_occupied()
271
+
272
+ for _ in range(cma_generations):
273
+ optimize_cma_(archive, fitness, bounds, rg, cma_params)
274
+
275
+ def optimize_cma_(archive, fitness, bounds, rg, cma_params):
276
+ select_n = cma_params.get('best_n', 100)
277
+ x0, y, iter = archive.random_xs_one(select_n, rg)
278
+ sigma = cma_params.get('sigma',rg.uniform(0.03, 0.3)**2)
279
+ popsize = cma_params.get('popsize', 31)
280
+ es = cmaescpp.ACMA_C(archive.dim, bounds, x0 = x0,
281
+ popsize = popsize, input_sigma = sigma, rg = rg)
282
+ maxiters = cma_params.get('maxiters', 100)
283
+ stall_criterion = cma_params.get('stall_criterion', 5)
284
+ old_ys = None
285
+ last_improve = 0
286
+ for iter in range(maxiters):
287
+ xs = es.ask()
288
+ improvement, ys = update_archive(archive, xs, fitness)
289
+ if iter > 0:
290
+ if (np.sort(ys) < old_ys).any():
291
+ last_improve = iter
292
+ if last_improve + stall_criterion < iter:
293
+ # no improvement
294
+ break
295
+ if es.tell(improvement) != 0:
296
+ break
297
+ old_ys = np.sort(ys)
298
+
299
+ @np.errstate(invalid='ignore')
300
+ def update_archive(archive: Archive, xs: np.ndarray,
301
+ fitness: Optional[Callable[[ArrayLike], Tuple[float, np.ndarray]]] = None,
302
+ yds: Optional[ArrayLike] = None):
303
+ # evaluate population, update archive and determine ranking
304
+ popsize = len(xs)
305
+ if yds is None:
306
+ yds = [fitness(x) for x in xs]
307
+ descs = np.array([yd[1] for yd in yds])
308
+ niches = archive.index_of_niches(descs)
309
+ # real values
310
+ ys = np.fromiter((yd[0] for yd in yds), dtype=float)
311
+ oldys = np.fromiter((archive.get_y(niches[i]) for i in range(popsize)), dtype=float)
312
+ improvement = ys - oldys
313
+ neg = np.argwhere(improvement < 0)
314
+ if len(neg) > 0:
315
+ neg = neg.reshape((len(neg)))
316
+ # update archive for all real improvements
317
+ for i in neg:
318
+ archive.set(niches[i], yds[i], xs[i])
319
+ # prioritize empty niches
320
+ empty = (improvement == -np.inf) # these need to be sorted according to fitness
321
+ occupied = np.logical_not(empty)
322
+ min_valid = np.amin(improvement[occupied]) if sum(occupied) > 0 else 0
323
+ norm_ys = ys[empty] - np.amax(ys) - 1E-9
324
+ improvement[empty] = min_valid + norm_ys
325
+ # return both improvement compared to archive elites and real fitness
326
+ return improvement, ys
327
+
328
+ @njit()
329
+ def get_grid_indices(ds, capacity, lb, ub):
330
+ rdim = int(capacity ** (1/ds.shape[1]) + 0.5)
331
+ ds_norm = (ds - lb) / (ub - lb)
332
+ indices = np.empty(len(ds), dtype=np.int32)
333
+ for i, d in enumerate(ds_norm):
334
+ index = 0
335
+ f = 1
336
+ for di in d:
337
+ index += f * int(rdim*di)
338
+ f *= rdim
339
+ indices[i] = max(0, min(capacity-1, int(index)))
340
+ return indices
341
+
342
+ class Archive(object):
343
+ """Multi-processing map elites archive.
344
+ Stores decision vectors, fitness values ,
345
+ description vectors, niche centers and statistics for the x-values"""
346
+
347
+ def __init__(self,
348
+ dim: int,
349
+ qd_bounds: Bounds,
350
+ capacity: int,
351
+ name: Optional[str] = "",
352
+ use_stats = False
353
+ ):
354
+ """Creates an empty archive."""
355
+ self.dim = dim
356
+ self.qd_dim = len(qd_bounds.lb)
357
+ self.qd_bounds = Bounds(np.array(qd_bounds.lb), np.array(qd_bounds.ub))
358
+ self.desc_lb = self.qd_bounds.lb
359
+ self.desc_scale = self.qd_bounds.ub - self.qd_bounds.lb
360
+ self.capacity = capacity
361
+ self.name = name
362
+ self.cs = None
363
+ self.lock = mp.Lock()
364
+ self.use_stats = use_stats
365
+ self.reset()
366
+
367
+ def reset(self):
368
+ """Resets all submitted solutions but keeps the niche centers."""
369
+ self.xs = Shared2d(np.empty((self.capacity, self.dim), dtype = np.float64))
370
+ self.ds = Shared2d(np.empty((self.capacity, self.qd_dim), dtype = np.float64))
371
+ self.create_views()
372
+ self.ys = mp.RawArray(ct.c_double, self.capacity)
373
+ self.counts = mp.RawArray(ct.c_long, self.capacity) # count
374
+ self.occupied = mp.RawValue(ct.c_long, 0)
375
+ self.stats = mp.RawArray(ct.c_double, self.capacity * self.dim * 4 if self.use_stats else 0)
376
+ for i in range(self.capacity):
377
+ self.counts[i] = 0
378
+ self.set_y(i, np.inf)
379
+ self.ds_view[i] = np.full(self.qd_dim, np.inf)
380
+ if self.stats:
381
+ self.set_stat(i, 0, np.zeros(self.dim)) # mean
382
+ self.set_stat(i, 1, np.zeros(self.dim)) # qmean
383
+ self.set_stat(i, 2, np.full(self.dim, np.inf)) # min
384
+ self.set_stat(i, 3, np.full(self.dim, -np.inf)) # max
385
+
386
+ def init_niches(self, samples_per_niche: int = 10):
387
+ """Computes the niche centers using KMeans and builds the KDTree for niche determination."""
388
+ # If samples_per_niche > 0 cvt-clustering is used, else grid-clustering is used.
389
+ self.cvt_clustering = samples_per_niche > 0
390
+ if self.cvt_clustering:
391
+ set_KDTree(self, None, self.capacity, self.qd_bounds, samples_per_niche)
392
+ self.cs = mp.RawArray(ct.c_double, self.capacity * self.qd_dim)
393
+ self.set_cs(self.centers)
394
+
395
+ def get_occupied_data(self):
396
+ ys = self.get_ys()
397
+ occupied = (ys < np.inf)
398
+ return ys[occupied], self.ds_view[occupied], self.xs_view[occupied]
399
+
400
+ def join(self, archive: Archive):
401
+ ys, ds, xs = archive.get_occupied_data()
402
+ niches = archive.index_of_niches(ds)
403
+ yds = np.array([(y, d) for y, d in zip(ys, ds)])
404
+ for i in range(len(ys)):
405
+ archive.set(niches[i], yds[i], xs[i])
406
+ archive.argsort()
407
+
408
+ def fname(self, name):
409
+ """Archive file name."""
410
+ return f'arch.{name}.{self.capacity}.{self.dim}.{self.qd_dim}'
411
+
412
+ def save(self, name: str):
413
+ """Saves the archive to disc."""
414
+ np.savez_compressed(self.fname(name),
415
+ xs=self.xs_view,
416
+ ds=self.ds_view,
417
+ ys=self.get_ys(),
418
+ cs=self.get_cs() if self.cvt_clustering else np.empty(0),
419
+ stats=self.get_stats(),
420
+ counts=self.get_counts()
421
+ )
422
+
423
+ def load(self, name: str):
424
+ """Loads the archive from disc."""
425
+ self.cs = mp.RawArray(ct.c_double, self.capacity * self.qd_dim)
426
+ with np.load(self.fname(name) + '.npz') as data:
427
+ self.cvt_clustering = len(data['cs']) > 0
428
+ xs = data['xs']
429
+ ds = data['ds']
430
+ self.xs.view()[:] = xs
431
+ self.ds.view()[:] = ds
432
+ self.set_ys(data['ys'])
433
+ if self.cvt_clustering:
434
+ self.set_cs(data['cs'])
435
+ self.counts[:] = data['counts']
436
+ stats = data['stats']
437
+ if len(stats) == len(self.stats):
438
+ self.set_stats(stats)
439
+ self.occupied.value = np.count_nonzero(self.get_ys() < np.inf)
440
+ self.dim = xs.shape[1]
441
+ self.qd_dim = ds.shape[1]
442
+ self.capacity = xs.shape[0]
443
+ if self.cvt_clustering:
444
+ set_KDTree(self, self.get_cs(), None, None, None)
445
+
446
+ def index_of_niches(self, ds):
447
+ if hasattr(self, "kdt"): # use k-means clusters
448
+ return self.kdt.query(self.encode_d(ds), k=1, sort_results=False)[1].T[0]
449
+ else: # use grid based clustering
450
+ return get_grid_indices(ds, self.capacity, self.qd_bounds.lb, self.qd_bounds.ub)
451
+
452
+ def in_niche_filter(self,
453
+ fit: Callable[[ArrayLike], float],
454
+ index: int):
455
+ """Creates a fitness function wrapper rejecting out of niche arguments."""
456
+ return in_niche_filter(fit, index, self.index_of_niches)
457
+
458
+ def set(self,
459
+ i: int,
460
+ yd: np.ndarray,
461
+ x: np.ndarray):
462
+ """Adds a solution to the archive if it improves the corresponding niche.
463
+ Updates solution."""
464
+ self.update_stats(i, x)
465
+ y, d = yd
466
+ # register improvement
467
+ yold = self.get_y(i)
468
+ if y < yold:
469
+ if yold == np.inf: # not yet occupied
470
+ self.occupied.value += 1
471
+ self.set_y(i, y)
472
+ self.xs_view[i] = x
473
+ self.ds_view[i] = d
474
+
475
+ def update_stats(self,
476
+ i: int,
477
+ x: np.ndarray):
478
+ """Updates solution statistics."""
479
+ with self.lock:
480
+ self.counts[i] += 1
481
+ count = self.counts[i]
482
+ if self.use_stats:
483
+ mean = self.get_x_mean(i)
484
+ diff = x - mean
485
+ self.set_stat(i, 0, mean + diff * (1./count)) # mean
486
+ self.set_stat(i, 1, self.get_stat(i, 1) + np.multiply(diff,diff) * ((count-1)/count)) # qmean
487
+ self.set_stat(i, 2, np.minimum(x, self.get_stat(i, 2))) # min
488
+ self.set_stat(i, 3, np.maximum(x, self.get_stat(i, 3))) # max
489
+
490
+ def get_occupied(self) -> int:
491
+ return self.occupied.value
492
+
493
+ def get_count(self, i: int) -> int:
494
+ return self.counts[i]
495
+
496
+ def get_counts(self) -> np.ndarray:
497
+ return np.array(self.counts[:])
498
+
499
+ def get_x_mean(self, i: int) -> np.ndarray:
500
+ return self.get_stat(i, 0)
501
+
502
+ def get_x_stdev(self, i: int) -> np.ndarray:
503
+ count = self.get_count(i)
504
+ if count == 0:
505
+ return np.zeros(self.dim)
506
+ else:
507
+ qmean = np.array(self.get_stat(i, 1))
508
+ return np.sqrt(qmean * (1./count))
509
+
510
+ def get_x_min(self, i: int) -> np.ndarray:
511
+ return self.get_stat(i, 2)
512
+
513
+ def get_x_max(self, i: int) -> np.ndarray:
514
+ return self.get_stat(i, 3)
515
+
516
+ def create_views(self): # needs to be called in the target process
517
+ self.xs_view = self.xs.view()
518
+ self.ds_view = self.ds.view()
519
+
520
+ def encode_d(self, d):
521
+ return (d - self.desc_lb) / self.desc_scale
522
+
523
+ def decode_d(self, d):
524
+ return (d * self.desc_scale) + self.desc_lb
525
+
526
+ def get_xs(self) -> np.ndarray:
527
+ return self.xs.view()
528
+
529
+ def get_ds(self) -> np.ndarray:
530
+ return self.ds.view()
531
+
532
+ def get_y(self, i: int) -> float:
533
+ return self.ys[i]
534
+
535
+ def get_ys(self) -> np.ndarray:
536
+ return np.array(self.ys[:])
537
+
538
+ def get_qd_score(self) -> float:
539
+ ys = self.get_ys()
540
+ occupied = (ys != np.inf)
541
+ ys = ys[occupied]
542
+ if len(ys) == 0:
543
+ return 0
544
+ min_y = np.amin(ys)
545
+ if min_y > 0: # if all y > 0 use sum of reciprocal
546
+ return np.sum(np.reciprocal(ys, where = ys!=0))
547
+ else: # else use only the negative ones
548
+ neg = (ys < 0)
549
+ ys = ys[neg]
550
+ return np.sum(-ys)
551
+
552
+ def set_y(self, i: int, y: float):
553
+ self.ys[i] = y
554
+
555
+ def set_ys(self, ys: ArrayLike):
556
+ for i in range(len(ys)):
557
+ self.set_y(i, ys[i])
558
+
559
+ def get_c(self, i: int) -> float:
560
+ return self.cs[i*self.qd_dim:(i+1)*self.qd_dim]
561
+
562
+ def get_cs(self) -> np.ndarray:
563
+ return np.array([self.get_c(i) for i in range(self.capacity)])
564
+
565
+ def get_cs_decoded(self) -> np.ndarray:
566
+ return self.decode_d(np.array([self.get_c(i) for i in range(self.capacity)]))
567
+
568
+ def set_c(self, i: int, c: float):
569
+ self.cs[i*self.qd_dim:(i+1)*self.qd_dim] = c[:]
570
+
571
+ def set_cs(self, cs: ArrayLike):
572
+ for i in range(len(cs)):
573
+ self.set_c(i, cs[i])
574
+
575
+ def get_stat(self, i: int, j: int) -> float:
576
+ p = 4*i+j
577
+ return self.stats[p*self.dim:(p+1)*self.dim]
578
+
579
+ def get_stats(self) -> np.ndarray:
580
+ return np.array(self.stats[:])
581
+
582
+ def set_stat(self, i: int, j: int, stat: ArrayLike):
583
+ p = 4*i+j
584
+ self.stats[p*self.dim:(p+1)*self.dim] = stat[:]
585
+
586
+ def set_stats(self, stats: ArrayLike):
587
+ self.stats[:] = stats[:]
588
+
589
+ def random_xs(self, best_n: int, chunk_size: int, rg: Generator) -> np.ndarray:
590
+ selection = rg.integers(0, best_n, chunk_size)
591
+ if best_n < self.capacity:
592
+ selection = np.fromiter((self.si[i] for i in selection), dtype=int)
593
+ return self.xs_view[selection]
594
+
595
+ def random_xs_one(self, best_n: int, rg: Generator) -> Tuple[np.ndarray, float, int]:
596
+ i = int(rg.random()*best_n)
597
+ return self.get_x(i), self.get_y(i), i
598
+
599
+ def argsort(self) -> np.ndarray:
600
+ """Sorts the archive according to its niche values."""
601
+ self.si = np.argsort(self.get_ys())
602
+ return self.si
603
+
604
+ def dump(self, n: Optional[int] = None):
605
+ """Dumps the archive content."""
606
+ if n is None:
607
+ n = self.capacity
608
+ ys = self.get_ys()
609
+ si = np.argsort(ys)
610
+ for i in range(n):
611
+ print(si[i], ys[si[i]], self.get_d(si[i]), self.get_x(si[i]))
612
+
613
+ def info(self) -> str:
614
+ occ = self.get_occupied()
615
+ score = self.get_qd_score()
616
+ best_y = np.amin(self.get_ys())
617
+ count = np.sum(self.get_counts())
618
+ return f'{occ} {score:.3f} {best_y:.3f} {count}'
619
+
620
+ class wrapper(object):
621
+ """Fitness function wrapper for multi processing logging."""
622
+
623
+ def __init__(self,
624
+ fit:Callable[[ArrayLike], Tuple[float, np.ndarray]],
625
+ qd_dim: int,
626
+ interval: Optional[int] = 1000000,
627
+ save_interval: Optional[int] = 1E20):
628
+
629
+ self.fit = fit
630
+ self.evals = mp.RawValue(ct.c_int, 0)
631
+ self.best_y = mp.RawValue(ct.c_double, np.inf)
632
+ self.t0 = perf_counter()
633
+ self.qd_dim = qd_dim
634
+ self.interval = interval
635
+ self.save_interval = save_interval
636
+ self.lock = mp.Lock()
637
+
638
+ def __call__(self, x: ArrayLike):
639
+ try:
640
+ if np.isnan(x).any():
641
+ return np.inf, np.zeros(self.qd_dim)
642
+ with self.lock:
643
+ self.evals.value += 1
644
+ log = self.evals.value % self.interval == 0
645
+ save = self.evals.value % self.save_interval == 0
646
+ y, desc = self.fit(x)
647
+ if np.isnan(y) or np.isnan(desc).any():
648
+ return np.inf, np.zeros(self.qd_dim)
649
+ y0 = y if np.isscalar(y) else sum(y)
650
+ if y0 < self.best_y.value:
651
+ self.best_y.value = y0
652
+ log = True
653
+ if log:
654
+ archinfo = self.archive.info() if hasattr(self, 'archive') else ''
655
+ logger.info(
656
+ f'{dtime(self.t0)} {archinfo} {self.evals.value:.0f} {self.evals.value/(1E-9 + dtime(self.t0)):.0f} {self.best_y.value:.3f} {list(x)}')
657
+ if save and hasattr(self, 'archive'):
658
+ self.archive.save(f'{self.evals.value}')
659
+ return y, desc
660
+ except Exception as ex:
661
+ print(str(ex))
662
+ return np.inf, np.zeros(self.qd_dim)
663
+
664
+ class in_niche_filter(object):
665
+ """Fitness function wrapper rejecting out of niche arguments."""
666
+
667
+ def __init__(self,
668
+ fit:Callable[[ArrayLike], Tuple[float, np.ndarray]],
669
+ index: int,
670
+ index_of_niches: Callable[[ArrayLike], np.ndarray]):
671
+ self.fit = fit
672
+ self.index_of_niches = index_of_niches
673
+ self.index = index
674
+
675
+ def __call__(self, x: ArrayLike) -> float:
676
+ y, desc = self.fit(x)
677
+ if self.index_of_niches([desc])[0] == self.index:
678
+ return y
679
+ else:
680
+ return np.inf
681
+
682
+ def variation_(pop, lower, upper, rg, dis_c = 20, dis_m = 20):
683
+ """Generate offspring individuals using SBX (Simulated Binary Crossover) and mutation."""
684
+ dis_c *= 0.5 + 0.5*rg.random() # vary spread factors randomly
685
+ dis_m *= 0.5 + 0.5*rg.random()
686
+ pop = pop[:(len(pop) // 2) * 2][:]
687
+ (n, d) = np.shape(pop)
688
+ parent_1 = pop[:n // 2, :]
689
+ parent_2 = pop[n // 2:, :]
690
+ beta = np.zeros((n // 2, d))
691
+ mu = rg.random((n // 2, d))
692
+ beta[mu <= 0.5] = np.power(2 * mu[mu <= 0.5], 1 / (dis_c + 1))
693
+ beta[mu > 0.5] = np.power(2 * mu[mu > 0.5], -1 / (dis_c + 1))
694
+ beta = beta * ((-1)** rg.integers(2, size=(n // 2, d)))
695
+ beta[rg.random((n // 2, d)) < 0.5] = 1
696
+ parent_mean = (parent_1 + parent_2) / 2
697
+ parent_diff = (parent_1 - parent_2) / 2
698
+ offspring = np.vstack((parent_mean + beta * parent_diff, parent_mean - beta * parent_diff))
699
+ site = rg.random((n, d)) < 1.0 / d
700
+ mu = rg.random((n, d))
701
+ temp = site & (mu <= 0.5)
702
+ lower, upper = np.tile(lower, (n, 1)), np.tile(upper, (n, 1))
703
+ norm = (offspring[temp] - lower[temp]) / (upper[temp] - lower[temp])
704
+ offspring[temp] += (upper[temp] - lower[temp]) * \
705
+ (np.power(2. * mu[temp] + (1. - 2. * mu[temp]) * np.power(np.abs(1. - norm), dis_m + 1.),
706
+ 1. / (dis_m + 1)) - 1.)
707
+ temp = site & (mu > 0.5)
708
+ norm = (upper[temp] - offspring[temp]) / (upper[temp] - lower[temp])
709
+ offspring[temp] += (upper[temp] - lower[temp]) * \
710
+ (1. - np.power(
711
+ 2. * (1. - mu[temp]) + 2. * (mu[temp] - 0.5) * np.power(np.abs(1. - norm), dis_m + 1.),
712
+ 1. / (dis_m + 1.)))
713
+ return np.clip(offspring, lower, upper)
714
+
715
+ def iso_dd_(x1, x2, lower, upper, rg, iso_sigma = 0.01, line_sigma = 0.2):
716
+ """Generate offspring individuals using Iso+Line."""
717
+ a = rg.normal(0, iso_sigma, x1.shape)
718
+ b = rg.normal(0, line_sigma, x2.shape)
719
+ z = x1 + a + np.multiply(b, (x1 - x2))
720
+ return np.clip(z, lower, upper)
721
+
722
+ def get_centers_(niche_num, dim, samples_per_niche):
723
+ p = Path('voronoi_cache')
724
+ p.mkdir(exist_ok=True)
725
+ fname = f'centers_{niche_num}_{dim}_{samples_per_niche}.npz'
726
+ files = p.glob(fname)
727
+ for file in files: # if cached just load
728
+ with np.load(file) as data:
729
+ return data['cs']
730
+ else:
731
+ descs = rng.uniform(0, 1, (niche_num*samples_per_niche, dim))
732
+ # Applies KMeans to the random samples determine the centers of each niche."""
733
+ k_means = KMeans(init='k-means++', n_clusters=niche_num, n_init=1, verbose=1)
734
+ k_means.fit(descs)
735
+ centers = k_means.cluster_centers_
736
+ np.savez_compressed(f'voronoi_cache/centers_{niche_num}_{dim}_{samples_per_niche}', cs=centers)
737
+ return centers