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/__init__.py +12 -2
- fcmaes/advretry.py +217 -159
- fcmaes/astro.py +143 -27
- fcmaes/bitecpp.py +107 -0
- fcmaes/cmaes.py +204 -173
- fcmaes/cmaescpp.py +253 -87
- fcmaes/crfmnes.py +339 -0
- fcmaes/crfmnescpp.py +273 -0
- fcmaes/dacpp.py +39 -51
- fcmaes/de.py +472 -0
- fcmaes/decpp.py +222 -64
- fcmaes/diversifier.py +357 -0
- fcmaes/evaluator.py +297 -14
- fcmaes/lib/libacmalib.dll +0 -0
- fcmaes/lib/libacmalib.dylib +0 -0
- fcmaes/lib/libacmalib.so +0 -0
- fcmaes/lib/libhbv.so +0 -0
- fcmaes/lib/liblrgv.so +0 -0
- fcmaes/lib/librw_top_trumps.dll +0 -0
- fcmaes/lib/librw_top_trumps.so +0 -0
- fcmaes/mapelites.py +737 -0
- fcmaes/mode.py +719 -0
- fcmaes/modecpp.py +470 -0
- fcmaes/moretry.py +270 -0
- fcmaes/multiretry.py +195 -0
- fcmaes/optimizer.py +883 -112
- fcmaes/pgpecpp.py +340 -0
- fcmaes/pygmoretry.py +10 -19
- fcmaes/retry.py +248 -121
- fcmaes/test_cma.py +207 -30
- fcmaes/testfun.py +38 -1
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/METADATA +22 -12
- fcmaes-1.6.9.dist-info/RECORD +36 -0
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/WHEEL +1 -1
- fcmaes/hhcpp.py +0 -114
- fcmaes/lib/libgtoplib.dll +0 -0
- fcmaes/lib/libgtoplib.so +0 -0
- fcmaes-1.1.3.dist-info/RECORD +0 -23
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/LICENSE +0 -0
- {fcmaes-1.1.3.dist-info → fcmaes-1.6.9.dist-info}/top_level.txt +0 -0
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
|