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/modecpp.py ADDED
@@ -0,0 +1,470 @@
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
+
6
+ """Eigen based implementation of multi objective
7
+ Differential Evolution using the DE/pareto/1 strategy.
8
+ Derived and adapted for MO from its C++ counterpart
9
+ https://github.com/dietmarwo/fast-cma-es/blob/master/_fcmaescpp/deoptimizer.cpp
10
+
11
+ Can switch to NSGA-II like population update via parameter 'nsga_update'.
12
+ Then it works essentially like NSGA-II but instead of the tournament selection
13
+ the whole population is sorted and the best individuals survive. To do this
14
+ efficiently the crowd distance ordering is slightly inaccurate.
15
+
16
+ Supports parallel fitness function evaluation.
17
+
18
+ Features enhanced multiple constraint ranking (https://www.jstage.jst.go.jp/article/tjpnsec/11/2/11_18/_article/-char/en/)
19
+ improving its performance in handling constraints for engineering design optimization.
20
+
21
+ Enables the comparison of DE and NSGA-II population update mechanism with everything else
22
+ kept completely identical.
23
+
24
+ Requires python 3.5 or higher.
25
+
26
+ Uses the following deviation from the standard DE algorithm:
27
+ a) oscillating CR/F parameters.
28
+
29
+ You may keep parameters F and CR at their defaults since this implementation works well with the given settings for most problems,
30
+ since the algorithm oscillates between different F and CR settings.
31
+
32
+ For expensive objective functions (e.g. machine learning parameter optimization) use the workers
33
+ parameter to parallelize objective function evaluation. This causes delayed population update.
34
+ It is usually preferrable if popsize > workers and workers = mp.cpu_count() to improve CPU utilization.
35
+
36
+ The ints parameter is a boolean array indicating which parameters are discrete integer values. This
37
+ parameter was introduced after observing non optimal DE-results for the ESP2 benchmark problem:
38
+ https://github.com/AlgTUDelft/ExpensiveOptimBenchmark/blob/master/expensiveoptimbenchmark/problems/DockerCFDBenchmark.py
39
+ If defined it causes a "special treatment" for discrete variables: They are rounded to the next integer value and
40
+ there is an additional mutation to avoid getting stuck to local minima.
41
+
42
+ See https://github.com/dietmarwo/fast-cma-es/blob/master/tutorials/MODE.adoc for a detailed description.
43
+ """
44
+
45
+ import os
46
+ import time
47
+ import threadpoolctl
48
+ import ctypes as ct
49
+ import multiprocessing as mp
50
+ from multiprocessing import Process
51
+ import numpy as np
52
+ from scipy.optimize import Bounds
53
+ from fcmaes import mode, moretry
54
+ from fcmaes.mode import _filter, store
55
+ from numpy.random import Generator, PCG64DXSM, SeedSequence
56
+ from fcmaes.optimizer import dtime
57
+ from fcmaes.evaluator import mo_call_back_type, callback_mo, parallel_mo, libcmalib
58
+ from fcmaes.de import _check_bounds
59
+ from fcmaes.evaluator import is_debug_active
60
+ from loguru import logger
61
+ from typing import Optional, Callable, Tuple
62
+ from numpy.typing import ArrayLike
63
+
64
+ os.environ['MKL_DEBUG_CPU_TYPE'] = '5'
65
+
66
+ def minimize(mofun: Callable[[ArrayLike], ArrayLike],
67
+ nobj: int,
68
+ ncon: int,
69
+ bounds: Bounds,
70
+ guess: Optional[np.ndarray] = None,
71
+ popsize: Optional[int] = 64,
72
+ max_evaluations: Optional[int] = 100000,
73
+ workers: Optional[int] = 1,
74
+ f: Optional[float] = 0.5,
75
+ cr: Optional[float] = 0.9,
76
+ pro_c: Optional[float] = 0.5,
77
+ dis_c: Optional[float] = 15.0,
78
+ pro_m: Optional[float] = 0.9,
79
+ dis_m: Optional[float] = 20.0,
80
+ nsga_update: Optional[bool] = True,
81
+ pareto_update: Optional[int] = 0,
82
+ ints: Optional[ArrayLike] = None,
83
+ min_mutate: Optional[float] = 0.1,
84
+ max_mutate: Optional[float] = 0.5,
85
+ rg: Optional[Generator] = Generator(PCG64DXSM()),
86
+ store: Optional[store] = None,
87
+ runid: Optional[int] = 0) -> Tuple[np.ndarray, np.ndarray]:
88
+
89
+ """Minimization of a multi objjective function of one or more variables using
90
+ Differential Evolution.
91
+
92
+ Parameters
93
+ ----------
94
+ mofun : callable
95
+ The objective function to be minimized.
96
+ ``mofun(x) -> ndarray(float)``
97
+ where ``x`` is an 1-D array with shape (n,)
98
+ nobj : int
99
+ number of objectives
100
+ ncon : int
101
+ number of constraints, default is 0.
102
+ The objective function needs to return vectors of size nobj + ncon
103
+ bounds : sequence or `Bounds`
104
+ Bounds on variables. There are two ways to specify the bounds:
105
+ 1. Instance of the `scipy.Bounds` class.
106
+ 2. Sequence of ``(min, max)`` pairs for each element in `x`. None
107
+ is used to specify no bound.
108
+ guess : ndarray, shape (popsize,dim) or Tuple
109
+ Initial guess.
110
+ popsize : int, optional
111
+ Population size.
112
+ max_evaluations : int, optional
113
+ Forced termination after ``max_evaluations`` function evaluations.
114
+ workers : int or None, optional
115
+ if workers > 1, function evaluation is performed in parallel for the whole population.
116
+ Useful for costly objective functions
117
+ f = float, optional
118
+ The mutation constant. In the literature this is also known as differential weight,
119
+ being denoted by F. Should be in the range [0, 2], usually leave at default.
120
+ cr = float, optional
121
+ The recombination constant. Should be in the range [0, 1].
122
+ In the literature this is also known as the crossover probability, usually leave at default.
123
+ pro_c, dis_c, pro_m, dis_m = float, optional
124
+ NSGA population update parameters, usually leave at default.
125
+ nsga_update = boolean, optional
126
+ Use of NSGA-II/SBX or DE population update. Default is True
127
+ pareto_update = float, optional
128
+ Only applied if nsga_update = False. Favor better solutions for sample generation. Default 0 -
129
+ use all population members with the same probability.
130
+ ints = list or array of bool, optional
131
+ indicating which parameters are discrete integer values. If defined these parameters will be
132
+ rounded to the next integer and some additional mutation of discrete parameters are performed.
133
+ min_mutate = float, optional
134
+ Determines the minimal mutation rate for discrete integer parameters.
135
+ max_mutate = float, optional
136
+ Determines the maximal mutation rate for discrete integer parameters.
137
+ rg = numpy.random.Generator, optional
138
+ Random generator for creating random guesses.
139
+ store : result store, optional
140
+ if defined the optimization results are added to the result store.
141
+
142
+ runid : int, optional
143
+ id used to identify the run for debugging / logging.
144
+
145
+ Returns
146
+ -------
147
+ x, y: list of argument vectors and corresponding value vectors of the optimization results. """
148
+
149
+ try:
150
+ mode = MODE_C(nobj, ncon, bounds, popsize, f, cr, pro_c, dis_c, pro_m, dis_m,
151
+ nsga_update, pareto_update, ints, min_mutate, max_mutate, rg, runid)
152
+ mode.set_guess(guess, mofun, rg)
153
+ if workers <= 1:
154
+ x, y = mode.minimize_ser(mofun, max_evaluations)
155
+ else:
156
+ x, y = mode.minimize_par(mofun, max_evaluations, workers)
157
+ if not store is None:
158
+ store.create_views()
159
+ store.add_results(x, y)
160
+ return x, y
161
+ except Exception as ex:
162
+ print(str(ex))
163
+ return None, None
164
+
165
+ def retry(mofun: Callable[[ArrayLike], ArrayLike],
166
+ nobj: int,
167
+ ncon: int,
168
+ bounds: Bounds,
169
+ guess: Optional[np.ndarray] = None,
170
+ num_retries: Optional[int] = 64,
171
+ popsize: Optional[int] = 64,
172
+ max_evaluations: Optional[int] = 100000,
173
+ workers: Optional[int] = mp.cpu_count(),
174
+ nsga_update: Optional[bool] = False,
175
+ pareto_update: Optional[int] = 0,
176
+ ints: Optional[ArrayLike] = None,
177
+ capacity: Optional[int] = None):
178
+
179
+ """Minimization of a multi objjective function of one or more variables using parallel
180
+ optimization retry.
181
+
182
+ Parameters
183
+ ----------
184
+ mofun : callable
185
+ The objective function to be minimized.
186
+ ``mofun(x, *args) -> ndarray(float)``
187
+ where ``x`` is an 1-D array with shape (n,) and ``args``
188
+ is a tuple of the fixed parameters needed to completely
189
+ specify the function.
190
+ nobj : int
191
+ number of objectives
192
+ ncon : int
193
+ number of constraints, default is 0.
194
+ The objective function needs to return vectors of size nobj + ncon
195
+ bounds : sequence or `Bounds`
196
+ Bounds on variables. There are two ways to specify the bounds:
197
+ 1. Instance of the `scipy.Bounds` class.
198
+ 2. Sequence of ``(min, max)`` pairs for each element in `x`. None
199
+ is used to specify no bound.
200
+ guess : ndarray, shape (popsize,dim) or Tuple
201
+ Initial guess.
202
+ num_retries : int, optional
203
+ Number of optimization retries.
204
+ popsize : int, optional
205
+ Population size.
206
+ max_evaluations : int, optional
207
+ Forced termination after ``max_evaluations`` function evaluations.
208
+ workers : int or None, optional
209
+ If not workers is None, optimization is performed in parallel.
210
+ nsga_update = boolean, optional
211
+ Use of NSGA-II/SBX or DE population update. Default is False
212
+ pareto_update = float, optional
213
+ Only applied if nsga_update = False. Favor better solutions for sample generation. Default 0 -
214
+ use all population members with the same probability.
215
+ ints = list or array of bool, optional
216
+ indicating which parameters are discrete integer values. If defined these parameters will be
217
+ rounded to the next integer and some additional mutation of discrete parameters are performed.
218
+ capacity : int or None, optional
219
+ capacity of the store collecting all solutions. If full, its content is replaced by its
220
+ pareto front """
221
+
222
+ dim, _, _ = _check_bounds(bounds, None)
223
+ if capacity is None:
224
+ capacity = 2048*popsize
225
+ store = mode.store(dim, nobj + ncon, capacity)
226
+ sg = SeedSequence()
227
+ rgs = [Generator(PCG64DXSM(s)) for s in sg.spawn(workers)]
228
+ proc=[Process(target=_retry_loop,
229
+ args=(num_retries, pid, rgs, mofun, nobj, ncon, bounds, guess, popsize,
230
+ max_evaluations, workers, nsga_update, pareto_update,
231
+ store, ints))
232
+ for pid in range(workers)]
233
+ [p.start() for p in proc]
234
+ [p.join() for p in proc]
235
+ xs, ys = store.get_front()
236
+ return xs, ys
237
+
238
+ def _retry_loop(num_retries, pid, rgs, mofun, nobj, ncon, bounds, guess, popsize,
239
+ max_evaluations, workers, nsga_update, pareto_update,
240
+ store, ints):
241
+ store.create_views()
242
+ t0 = time.perf_counter()
243
+ num = max(1, num_retries - workers)
244
+ with threadpoolctl.threadpool_limits(limits=1, user_api="blas"):
245
+ while store.num_added.value < num:
246
+ minimize(mofun, nobj, ncon, bounds, guess, popsize,
247
+ max_evaluations = max_evaluations,
248
+ nsga_update=nsga_update, pareto_update=pareto_update,
249
+ rg = rgs[pid], store = store, ints=ints)
250
+ if is_debug_active():
251
+ logger.debug("retries = {0}: time = {1:.1f} i = {2}"
252
+ .format(store.num_added.value, dtime(t0), store.num_stored.value))
253
+
254
+ class MODE_C:
255
+
256
+ def __init__(self,
257
+ nobj: int,
258
+ ncon: int,
259
+ bounds: Bounds,
260
+ popsize: Optional[int] = 64,
261
+ f: Optional[float] = 0.5,
262
+ cr: Optional[float] = 0.9,
263
+ pro_c: Optional[float] = 0.5,
264
+ dis_c: Optional[float] = 15.0,
265
+ pro_m: Optional[float] = 0.9,
266
+ dis_m: Optional[float] = 20.0,
267
+ nsga_update: Optional[bool] = True,
268
+ pareto_update: Optional[int] = 0,
269
+ ints: Optional[ArrayLike] = None,
270
+ min_mutate: Optional[float] = 0.1,
271
+ max_mutate: Optional[float] = 0.5,
272
+ rg: Optional[Generator] = Generator(PCG64DXSM()),
273
+ runid: Optional[int] = 0):
274
+
275
+ """ Parameters
276
+ ----------
277
+ nobj : int
278
+ number of objectives
279
+ ncon : int
280
+ number of constraints, default is 0.
281
+ The objective function needs to return vectors of size nobj + ncon
282
+ bounds : sequence or `Bounds`
283
+ Bounds on variables. There are two ways to specify the bounds:
284
+ 1. Instance of the `scipy.Bounds` class.
285
+ 2. Sequence of ``(min, max)`` pairs for each element in `x`. None
286
+ is used to specify no bound.
287
+ popsize : int, optional
288
+ Population size.
289
+ f = float, optional
290
+ The mutation constant. In the literature this is also known as differential weight,
291
+ being denoted by F. Should be in the range [0, 2], usually leave at default.
292
+ cr = float, optional
293
+ The recombination constant. Should be in the range [0, 1].
294
+ In the literature this is also known as the crossover probability, usually leave at default.
295
+ pro_c, dis_c, pro_m, dis_m = float, optional
296
+ NSGA population update parameters, usually leave at default.
297
+ nsga_update = boolean, optional
298
+ Use of NSGA-II or DE population update. Default is True
299
+ pareto_update = float, optional
300
+ Only applied if nsga_update = False. Favor better solutions for sample generation. Default 0 -
301
+ use all population members with the same probability.
302
+ ints = list or array of bool, optional
303
+ indicating which parameters are discrete integer values. If defined these parameters will be
304
+ rounded to the next integer and some additional mutation of discrete parameters are performed.
305
+ min_mutate = float, optional
306
+ Determines the minimal mutation rate for discrete integer parameters.
307
+ max_mutate = float, optional
308
+ Determines the maximal mutation rate for discrete integer parameters.
309
+ rg = numpy.random.Generator, optional
310
+ Random generator for creating random guesses.
311
+ runid : int, optional
312
+ id used to identify the run for debugging / logging."""
313
+
314
+ dim, lower, upper = _check_bounds(bounds, None)
315
+ if popsize is None:
316
+ popsize = 64
317
+ if popsize % 2 == 1 and nsga_update: # nsga update requires even popsize
318
+ popsize += 1
319
+ if lower is None:
320
+ lower = [0]*dim
321
+ upper = [0]*dim
322
+ if ints is None or nsga_update: # nsga update doesn't support mixed integer
323
+ ints = [False]*dim
324
+ array_type = ct.c_double * dim
325
+ bool_array_type = ct.c_bool * dim
326
+ seed = int(rg.uniform(0, 2**32 - 1))
327
+ try:
328
+ self.ptr = initMODE_C(runid, dim, nobj, ncon, seed,
329
+ array_type(*lower), array_type(*upper), bool_array_type(*ints),
330
+ popsize, f, cr,
331
+ pro_c, dis_c, pro_m, dis_m,
332
+ nsga_update, pareto_update, min_mutate, max_mutate)
333
+ self.popsize = popsize
334
+ self.dim = dim
335
+ self.nobj = nobj
336
+ self.ncon = ncon
337
+ self.bounds = bounds
338
+ except Exception as ex:
339
+ print (str(ex))
340
+ pass
341
+
342
+ def __del__(self):
343
+ destroyMODE_C(self.ptr)
344
+
345
+ def set_guess(self, guess, mofun, rg = None):
346
+ if not guess is None:
347
+ if isinstance(guess, np.ndarray):
348
+ ys = np.array([mofun(x) for x in guess])
349
+ else:
350
+ guess, ys = guess
351
+ if rg is None:
352
+ rg = Generator(PCG64DXSM())
353
+ choice = rg.choice(len(ys), self.popsize,
354
+ replace = (len(ys) < self.popsize))
355
+ self.tell(ys[choice], guess[choice])
356
+
357
+ def ask(self) -> np.ndarray:
358
+ try:
359
+ popsize = self.popsize
360
+ n = self.dim
361
+ res = np.empty(popsize*n)
362
+ res_p = res.ctypes.data_as(ct.POINTER(ct.c_double))
363
+ askMODE_C(self.ptr, res_p)
364
+ xs = np.empty((popsize, n))
365
+ for p in range(popsize):
366
+ xs[p,:] = res[p*n : (p+1)*n]
367
+ return xs
368
+ except Exception as ex:
369
+ print (str(ex))
370
+ return None
371
+
372
+ def tell(self, ys: np.ndarray, xs: Optional[np.ndarray] = None) -> int:
373
+ try:
374
+ flat_ys = ys.flatten()
375
+ array_type_ys = ct.c_double * len(flat_ys)
376
+ if xs is None:
377
+ return tellMODE_C(self.ptr, array_type_ys(*flat_ys))
378
+ else:
379
+ flat_xs = xs.flatten()
380
+ array_type_xs = ct.c_double * len(flat_xs)
381
+ return setPopulationMODE_C(self.ptr, len(ys),
382
+ array_type_xs(*flat_xs), array_type_ys(*flat_ys))
383
+ except Exception as ex:
384
+ print (str(ex))
385
+ return -1
386
+
387
+ def tell_switch(self, ys: np.ndarray,
388
+ nsga_update: Optional[bool] = True,
389
+ pareto_update: Optional[int] = 0) -> int:
390
+ try:
391
+ flat_ys = ys.flatten()
392
+ array_type_ys = ct.c_double * len(flat_ys)
393
+ return tellMODE_switchC(self.ptr, array_type_ys(*flat_ys), nsga_update, pareto_update)
394
+ except Exception as ex:
395
+ print (ex)
396
+ return -1
397
+
398
+ def population(self) -> np.ndarray:
399
+ try:
400
+ popsize = self.popsize
401
+ n = self.dim
402
+ res = np.empty(popsize*n)
403
+ res_p = res.ctypes.data_as(ct.POINTER(ct.c_double))
404
+ populationMODE_C(self.ptr, res_p)
405
+ xs = np.empty((popsize, n))
406
+ for p in range(popsize):
407
+ xs[p,:] = res[p*n : (p+1)*n]
408
+ return xs
409
+ except Exception as ex:
410
+ print (str(ex))
411
+ return None
412
+
413
+ def minimize_ser(self,
414
+ fun: Callable[[ArrayLike], ArrayLike],
415
+ max_evaluations: Optional[int] = 100000) -> Tuple[np.ndarray, np.ndarray]:
416
+ evals = 0
417
+ stop = 0
418
+ while stop == 0 and evals < max_evaluations:
419
+ xs = self.ask()
420
+ ys = np.array([fun(x) for x in xs])
421
+ stop = self.tell(ys)
422
+ evals += self.popsize
423
+ return xs, ys
424
+
425
+ def minimize_par(self,
426
+ fun: Callable[[ArrayLike], ArrayLike],
427
+ max_evaluations: Optional[int] = 100000,
428
+ workers: Optional[int] = mp.cpu_count()) -> Tuple[np.ndarray, np.ndarray]:
429
+ fit = parallel_mo(fun, self.nobj + self.ncon, workers)
430
+ evals = 0
431
+ stop = 0
432
+ while stop == 0 and evals < max_evaluations:
433
+ xs = self.ask()
434
+ ys = fit(xs)
435
+ stop = self.tell(ys)
436
+ evals += self.popsize
437
+ fit.stop()
438
+ return xs, ys
439
+
440
+ if not libcmalib is None:
441
+
442
+ initMODE_C = libcmalib.initMODE_C
443
+ initMODE_C.argtypes = [ct.c_long, ct.c_int, ct.c_int, \
444
+ ct.c_int, ct.c_int, ct.POINTER(ct.c_double), ct.POINTER(ct.c_double), ct.POINTER(ct.c_bool), \
445
+ ct.c_int, ct.c_double, ct.c_double, ct.c_double, ct.c_double, ct.c_double, ct.c_double,
446
+ ct.c_bool, ct.c_double, ct.c_double, ct.c_double]
447
+
448
+ initMODE_C.restype = ct.c_void_p
449
+
450
+ destroyMODE_C = libcmalib.destroyMODE_C
451
+ destroyMODE_C.argtypes = [ct.c_void_p]
452
+
453
+ askMODE_C = libcmalib.askMODE_C
454
+ askMODE_C.argtypes = [ct.c_void_p, ct.POINTER(ct.c_double)]
455
+
456
+ tellMODE_C = libcmalib.tellMODE_C
457
+ tellMODE_C.argtypes = [ct.c_void_p, ct.POINTER(ct.c_double)]
458
+ tellMODE_C.restype = ct.c_int
459
+
460
+ tellMODE_switchC = libcmalib.tellMODE_switchC
461
+ tellMODE_switchC.argtypes = [ct.c_void_p, ct.POINTER(ct.c_double), ct.c_bool, ct.c_double]
462
+ tellMODE_switchC.restype = ct.c_int
463
+
464
+ populationMODE_C = libcmalib.populationMODE_C
465
+ populationMODE_C.argtypes = [ct.c_void_p, ct.POINTER(ct.c_double)]
466
+
467
+ setPopulationMODE_C = libcmalib.setPopulationMODE_C
468
+ setPopulationMODE_C.argtypes = [ct.c_void_p, ct.c_int, ct.POINTER(ct.c_double), ct.POINTER(ct.c_double)]
469
+ setPopulationMODE_C.restype = ct.c_int
470
+