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/moretry.py ADDED
@@ -0,0 +1,270 @@
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
+ # parallel optimization retry of a multi-objective problem.
7
+
8
+ import numpy as np
9
+ import math, sys, time, warnings, threadpoolctl
10
+ import multiprocessing as mp
11
+ from multiprocessing import Process
12
+ from scipy.optimize import Bounds
13
+ from numpy.random import Generator, PCG64DXSM, SeedSequence
14
+ from fcmaes.optimizer import de_cma, dtime, Optimizer
15
+ from fcmaes import retry, advretry
16
+ from loguru import logger
17
+
18
+ from typing import Optional, Callable, Tuple
19
+ from numpy.typing import ArrayLike
20
+
21
+ def minimize(fun: Callable[[ArrayLike], float],
22
+ bounds: Bounds,
23
+ weight_bounds: Bounds,
24
+ ncon: Optional[int] = 0,
25
+ value_exp: Optional[float] = 2.0,
26
+ value_limits: Optional[ArrayLike] = None,
27
+ num_retries: Optional[int] = 1024,
28
+ workers: Optional[int] = mp.cpu_count(),
29
+ popsize: Optional[int] = 31,
30
+ max_evaluations: Optional[int] = 50000,
31
+ capacity: Optional[int] = None,
32
+ optimizer: Optional[Optimizer] = None,
33
+ statistic_num: Optional[int] = 0,
34
+ plot_name: Optional[str] = None
35
+ ) -> Tuple[np.ndarray, np.ndarray]:
36
+ """Minimization of a multi objective function of one or more variables using parallel
37
+ optimization retry.
38
+
39
+ Parameters
40
+ ----------
41
+ fun : callable
42
+ The objective function to be minimized.
43
+ ``fun(x) -> float``
44
+ where ``x`` is an 1-D array with shape (n,)
45
+ bounds : sequence or `Bounds`, optional
46
+ Bounds on variables. There are two ways to specify the bounds:
47
+ 1. Instance of the `scipy.Bounds` class.
48
+ 2. Sequence of ``(min, max)`` pairs for each element in `x`. None
49
+ is used to specify no bound.
50
+ weight_bounds : `Bounds`, optional
51
+ Bounds on objective weights.
52
+ ncon : int, optional
53
+ number of constraints
54
+ value_exp : float, optional
55
+ exponent applied to the objective values for the weighted sum.
56
+ value_limits : sequence of floats, optional
57
+ Upper limit for optimized objective values to be stored.
58
+ num_retries : int, optional
59
+ Number of optimization retries.
60
+ workers : int, optional
61
+ number of parallel processes used. Default is mp.cpu_count()
62
+ popsize = int, optional
63
+ CMA-ES population size used for all CMA-ES runs.
64
+ Not used for differential evolution.
65
+ Ignored if parameter optimizer is defined.
66
+ max_evaluations : int, optional
67
+ Forced termination of all optimization runs after ``max_evaluations``
68
+ function evaluations. Only used if optimizer is undefined, otherwise
69
+ this setting is defined in the optimizer.
70
+ capacity : int, optional
71
+ capacity of the evaluation store.
72
+ optimizer : optimizer.Optimizer, optional
73
+ optimizer to use. Default is a sequence of differential evolution and CMA-ES.
74
+ plot_name : plot_name, optional
75
+ if defined the pareto front is plotted during the optimization to monitor progress
76
+
77
+ Returns
78
+ -------
79
+ xs, ys: list of argument vectors and corresponding value vectors of the optimization results. """
80
+
81
+ if optimizer is None:
82
+ optimizer = de_cma(max_evaluations, popsize)
83
+ if capacity is None:
84
+ capacity = num_retries
85
+ store = retry.Store(fun, bounds, capacity = capacity,
86
+ statistic_num = statistic_num)
87
+ store.plot_name = plot_name
88
+ xs = np.array(mo_retry(fun, weight_bounds, ncon, value_exp,
89
+ store, optimizer.minimize, num_retries, value_limits, workers))
90
+ ys = np.array([fun(x) for x in xs])
91
+ return xs, ys
92
+
93
+ def mo_retry(fun: Callable[[ArrayLike], float],
94
+ weight_bounds: Bounds,
95
+ ncon: int,
96
+ y_exp: float,
97
+ store,
98
+ optimize: Callable,
99
+ num_retries: int,
100
+ value_limits: ArrayLike,
101
+ workers: Optional[int] = mp.cpu_count()):
102
+
103
+ sg = SeedSequence()
104
+ rgs = [Generator(PCG64DXSM(s)) for s in sg.spawn(workers)]
105
+ proc=[Process(target=_retry_loop,
106
+ args=(pid, rgs, fun, weight_bounds, ncon, y_exp,
107
+ store, optimize, num_retries, value_limits)) for pid in range(workers)]
108
+ [p.start() for p in proc]
109
+ [p.join() for p in proc]
110
+ store.sort()
111
+ store.dump()
112
+ return store.xs_view
113
+
114
+ def _retry_loop(pid, rgs, fun, weight_bounds, ncon, y_exp,
115
+ store, optimize, num_retries, value_limits):
116
+
117
+ store.create_xs_view()
118
+ lower = store.lower
119
+ wlb = np.array(weight_bounds.lb)
120
+ wub = np.array(weight_bounds.ub)
121
+ with threadpoolctl.threadpool_limits(limits=1, user_api="blas"):
122
+ while store.get_runs_compare_incr(num_retries):
123
+ try:
124
+ rg = rgs[pid]
125
+ w = rg.uniform(size=len(wub))
126
+ w /= _avg_exp(w, y_exp) # correct scaling
127
+ w = wlb + w * (wub - wlb)
128
+ wrapper = mo_wrapper(fun, w, ncon, y_exp)
129
+ x, y, evals = optimize(wrapper.eval, Bounds(store.lower, store.upper), None,
130
+ [rg.uniform(0.05, 0.1)]*len(lower), rg, store)
131
+ objs = wrapper.mo_eval(x) # retrieve the objective values
132
+ if value_limits is None or all([objs[i] < value_limits[i] for i in range(len(w))]):
133
+ store.add_result(y, x, evals, np.inf)
134
+ if not store.plot_name is None:
135
+ name = store.plot_name + "_moretry_" + str(store.get_count_evals())
136
+ xs = np.array(store.get_xs())
137
+ ys = np.array([fun(x) for x in xs])
138
+ np.savez_compressed(name, xs=xs, ys=ys)
139
+ plot(name, ncon, xs, ys)
140
+ except Exception as ex:
141
+ print(str(ex))
142
+
143
+ def pareto(xs: np.ndarray, ys: np.ndarray):
144
+ """pareto front for argument vectors and corresponding function value vectors."""
145
+ par = _pareto(ys)
146
+ xp = xs[par]
147
+ yp = ys[par]
148
+ ya = np.argsort(yp.T[0])
149
+ return xp[ya], yp[ya]
150
+
151
+ class mo_wrapper(object):
152
+ """wrapper for multi objective functions applying the weighted sum approach."""
153
+
154
+ def __init__(self, fun, weights, ncon, y_exp=2):
155
+ self.fun = fun
156
+ self.weights = weights
157
+ self.ny = len(weights)
158
+ self.nobj = self.ny - ncon
159
+ self.ncon = ncon
160
+ self.y_exp = y_exp
161
+
162
+ def eval(self, x):
163
+ y = self.fun(np.array(x))
164
+ weighted = _avg_exp(self.weights*y, self.y_exp)
165
+ if self.ncon > 0: # check constraint violations
166
+ violations = np.fromiter((i for i in range(self.nobj, self.ny) if y[i] > 0), dtype=int)
167
+ if len(violations) > 0:
168
+ weighted += sum(self.weights[violations])
169
+ return weighted
170
+
171
+ def mo_eval(self, x):
172
+ return self.fun(np.array(x))
173
+
174
+ def minimize_plot(name: str,
175
+ optimizer: Optimizer,
176
+ fun: Callable[[ArrayLike], float],
177
+ bounds: Bounds,
178
+ weight_bounds,
179
+ ncon: Optional[int] = 0,
180
+ value_limits: Optional[ArrayLike] = None,
181
+ num_retries: Optional[int] = 1024,
182
+ exp: Optional[float] = 2.0,
183
+ workers: Optional[int] = mp.cpu_count(),
184
+ statistic_num = 0, plot_name = None):
185
+
186
+ time0 = time.perf_counter() # optimization start time
187
+ name += '_' + optimizer.name
188
+ logger.info('optimize ' + name)
189
+ xs, ys = minimize(fun, bounds, weight_bounds, ncon,
190
+ value_exp = exp,
191
+ value_limits = value_limits,
192
+ num_retries = num_retries,
193
+ optimizer = optimizer,
194
+ workers = workers,
195
+ statistic_num = statistic_num, plot_name = plot_name)
196
+ logger.info(name + ' time ' + str(dtime(time0)))
197
+ np.savez_compressed(name, xs=xs, ys=ys)
198
+ plot(name, ncon, xs, ys)
199
+
200
+ def plot(name, ncon, xs, ys, eps = 1E-2, all=True, interp=False, plot3d=False):
201
+ try:
202
+ if ncon > 0: # select feasible
203
+ ycon = np.array([np.maximum(y[-ncon:], 0) for y in ys])
204
+ con = np.sum(ycon, axis=1)
205
+ nobj = len(ys[0]) - ncon
206
+ feasible = np.fromiter((i for i in range(len(ys)) if con[i] < eps), dtype=int)
207
+ if len(feasible) > 0:
208
+ xs, ys = xs[feasible], np.array([y[:nobj] for y in ys[feasible]])
209
+ else:
210
+ print("no feasible")
211
+ return
212
+ if all:
213
+ retry.plot(ys, 'all_' + name + '.png', interp=False)
214
+ xs, ys = pareto(xs, ys)
215
+ for x, y in zip(xs, ys):
216
+ print(str(list(y)) + ' ' + str([round(xi,5) for xi in x]))
217
+ retry.plot(ys, 'front_' + name + '.png', interp=interp, plot3d=plot3d)
218
+ except Exception as ex:
219
+ print(str(ex))
220
+
221
+ def adv_minimize_plot(name: str,
222
+ optimizer: Optimizer,
223
+ fun: Callable[[ArrayLike], float],
224
+ bounds: Optional[Bounds],
225
+ value_limit: Optional[float] = np.inf,
226
+ num_retries: Optional[int] = 1024,
227
+ statistic_num: Optional[int] = 0):
228
+
229
+ time0 = time.perf_counter() # optimization start time
230
+ name += '_smart_' + optimizer.name
231
+ logger.info('smart optimize ' + name)
232
+ store = advretry.Store(lambda x:fun(x)[0], bounds, capacity=5000,
233
+ num_retries=num_retries, statistic_num = statistic_num)
234
+ advretry.retry(store, optimizer.minimize, value_limit)
235
+ xs = np.array(store.get_xs())
236
+ ys = np.fromiter((fun(x) for x in xs), dtype=float)
237
+ retry.plot(ys, '_all_' + name + '.png', interp=False)
238
+ np.savez_compressed(name , xs=xs, ys=ys)
239
+ xs, front = pareto(xs, ys)
240
+ logger.info(name+ ' time ' + str(dtime(time0)))
241
+ retry.plot(front, '_front_' + name + '.png')
242
+
243
+ def _avg_exp(y, y_exp):
244
+ with warnings.catch_warnings():
245
+ warnings.simplefilter("ignore")
246
+ weighted = sum([y[i]**y_exp for i in range(len(y))])**(1.0/y_exp)
247
+ return weighted
248
+
249
+ def _pareto_values(ys):
250
+ ys = ys[ys.sum(1).argsort()[::-1]]
251
+ undominated = np.ones(ys.shape[0], dtype=bool)
252
+ for i in range(ys.shape[0]):
253
+ n = ys.shape[0]
254
+ if i >= n:
255
+ break
256
+ undominated[i+1:n] = (ys[i+1:] >= ys[i]).any(1)
257
+ ys = ys[undominated[:n]]
258
+ return ys
259
+
260
+ def _pareto(ys):
261
+ pareto = np.arange(ys.shape[0])
262
+ index = 0 # Next index to search for
263
+ while index < len(ys):
264
+ mask = np.any(ys < ys[index], axis=1)
265
+ mask[index] = True
266
+ pareto = pareto[mask] # Remove dominated points
267
+ ys = ys[mask]
268
+ index = np.sum(mask[:index])+1
269
+ return pareto
270
+
fcmaes/multiretry.py ADDED
@@ -0,0 +1,195 @@
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
+ # parallel optimization retry of a list of problems.
7
+
8
+ import numpy as np
9
+ import _pickle as cPickle
10
+ import bz2
11
+ import multiprocessing as mp
12
+ from scipy.optimize import OptimizeResult, Bounds
13
+ from fcmaes.optimizer import de_cma, eprint, Optimizer
14
+ from fcmaes import advretry
15
+
16
+ from fcmaes.evaluator import is_debug_active
17
+ from loguru import logger
18
+ from typing import Optional, Callable, Tuple, List
19
+ from numpy.typing import ArrayLike
20
+
21
+ def minimize(problems: ArrayLike,
22
+ ids: Optional[ArrayLike] = None,
23
+ retries_inc: Optional[int] = min(256, 8*mp.cpu_count()),
24
+ num_retries: Optional[int] = 10000,
25
+ keep: Optional[float] = 0.7,
26
+ optimizer: Optional[Optimizer] = de_cma(1500),
27
+ datafile = None) -> List:
28
+
29
+ """Minimization of a list of optimization problems by first applying parallel retry
30
+ to filter the best ones and then applying coordinated retry to evaluate these further.
31
+ Can replace mixed integer optimization if the integer variables are narrowly bound.
32
+ In this case all combinations of these integer values can be enumerated to generate a
33
+ list of problem instances each representing one combination. See for instance
34
+ https://www.esa.int/gsp/ACT/projects/gtop/tandem where there is a problem instance for each
35
+ planet sequence.
36
+
37
+ Parameters
38
+ ----------
39
+
40
+ problems: list
41
+ list of objects providing name, fun and bounds attributes like fcmaes.astro.Astrofun
42
+
43
+ ids: list, optional
44
+ list of objects corresponding to the list of problems used in logging to identify the
45
+ problem variant currently logged. If None, the index of the problem
46
+ variant is used instead.
47
+
48
+ retries_inc: int, optional
49
+ number of coordinated retries applied in the problem filter for each problem
50
+ in each iteration.
51
+
52
+ num_retries: int, optional
53
+ number of coordinated retries applied in the problem filter for the winner problem.
54
+
55
+ keep: float, optional
56
+ rate of the problems kept after each iteration. 100*(1 - keep) % will be deleted.
57
+
58
+ optimizer: optimizer.Optimizer, optional
59
+ optimizer to use for the problem filter.
60
+
61
+ datafile, optional
62
+ file to persist / retrieve the internal state of the optimizations.
63
+
64
+ Returns
65
+ -------
66
+ dictionary( optimizer -> ret): scipy.OptimizeResult
67
+ The optimization result is represented as an ``OptimizeResult`` object.
68
+ Important attributes are: ``x`` the solution array,
69
+ ``fun`` the best function value, ``nfev`` the number of function evaluations,
70
+ ``success`` a Boolean flag indicating if the optimizer exited successfully. """
71
+
72
+ solver = multiretry()
73
+ n = len(problems)
74
+
75
+ for i in range(n):
76
+ id = str(i+1) if ids is None else ids[i]
77
+ solver.add(problem_stats(problems[i], id, i, retries_inc, num_retries))
78
+
79
+ if not datafile is None:
80
+ solver.load(datafile)
81
+
82
+ while solver.size() > 1:
83
+ solver.retry(optimizer)
84
+ to_remove = int(round((1.0 - keep) * solver.size()))
85
+ if to_remove == 0 and keep < 1.0:
86
+ to_remove = 1
87
+ solver.remove_worst(to_remove)
88
+ solver.dump()
89
+ if not datafile is None:
90
+ solver.save(datafile)
91
+
92
+ idx = solver.values_all().argsort()
93
+ return list(np.asarray(solver.all_stats)[idx])
94
+
95
+ class problem_stats:
96
+
97
+ def __init__(self, prob, id, index, retries_inc = 64, num_retries = 10000):
98
+ self.store = advretry.Store(prob.fun, prob.bounds, num_retries=num_retries)
99
+ self.prob = prob
100
+ self.name = prob.name
101
+ self.fun = prob.fun
102
+ self.retries_inc = retries_inc
103
+ self.value = 0
104
+ self.id = id
105
+ self.index = index
106
+ self.ret = None
107
+ self.store.num_retries = self.retries_inc
108
+
109
+ def retry(self, optimizer):
110
+ self.store.num_retries += self.retries_inc
111
+ self.ret = advretry.retry(self.store, optimizer.minimize)
112
+ self.value = self.store.get_y_best()
113
+
114
+ class multiretry:
115
+
116
+ def __init__(self):
117
+ self.problem_stats = []
118
+ self.all_stats = []
119
+
120
+ def add(self, stats):
121
+ self.problem_stats.append(stats)
122
+ self.all_stats.append(stats)
123
+
124
+ def retry(self, optimizer):
125
+ for ps in self.problem_stats:
126
+ if is_debug_active():
127
+ logger.debug("problem " + ps.prob.name + ' ' + str(ps.id))
128
+ ps.retry(optimizer)
129
+
130
+ def values(self):
131
+ return np.fromiter((ps.value for ps in self.problem_stats), dtype=float)
132
+
133
+ def remove_worst(self, n = 1):
134
+ idx = self.values().argsort()
135
+ self.problem_stats = list(np.asarray(self.problem_stats)[idx])
136
+ for _ in range(n):
137
+ self.problem_stats.pop(-1)
138
+
139
+ def size(self):
140
+ return len(self.problem_stats)
141
+
142
+ def dump(self):
143
+ if is_debug_active():
144
+ for i in range(self.size()):
145
+ ps = self.problem_stats[i]
146
+ logger.debug(str(ps.id) + ' ' + str(ps.value))
147
+
148
+ def dump_all(self):
149
+ if is_debug_active():
150
+ idx = self.values_all().argsort()
151
+ self.all_stats = list(np.asarray(self.all_stats)[idx])
152
+ for i in range(len(self.all_stats)):
153
+ ps = self.all_stats[i]
154
+ logger.debug(str(ps.id) + ' ' + str(ps.value))
155
+
156
+ def values_all(self):
157
+ return np.fromiter((ps.value for ps in self.all_stats), dtype=float)
158
+
159
+ def result(self):
160
+ idx = self.values_all().argsort()
161
+ self.all_stats = list(np.asarray(self.all_stats)[idx])
162
+ ret = []
163
+ for i in range(len(self.all_stats)):
164
+ problem = self.all_stats[i].prob
165
+ store = self.all_stats[i].store
166
+ ret.append([problem,
167
+ OptimizeResult(x=store.get_x_best(), fun=store.get_y_best(),
168
+ nfev=store.get_count_evals(), success=True)])
169
+
170
+ # persist all stats
171
+ def save(self, name):
172
+ try:
173
+ with bz2.BZ2File(name + '.pbz2', 'w') as f:
174
+ cPickle.dump(self.get_data(), f)
175
+ except Exception as ex:
176
+ eprint('error writing data file ' + name + '.pbz2 ' + str(ex))
177
+
178
+ def load(self, name):
179
+ try:
180
+ data = cPickle.load(bz2.BZ2File(name + '.pbz2', 'rb'))
181
+ self.set_data(data)
182
+ except Exception as ex:
183
+ eprint('error reading data file ' + name + '.pbz2 ' + str(ex))
184
+
185
+ def get_data(self):
186
+ data = []
187
+ for stats in self.all_stats:
188
+ data.append(stats.store.get_data())
189
+ return data
190
+
191
+ def set_data(self, data):
192
+ for i in range(len(data)):
193
+ self.all_stats[i].store.set_data(data[i])
194
+
195
+