moospread 0.1.0__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.
Files changed (63) hide show
  1. moospread/__init__.py +3 -0
  2. moospread/core.py +1881 -0
  3. moospread/problem.py +193 -0
  4. moospread/tasks/__init__.py +4 -0
  5. moospread/tasks/dtlz_torch.py +139 -0
  6. moospread/tasks/mw_torch.py +274 -0
  7. moospread/tasks/re_torch.py +394 -0
  8. moospread/tasks/zdt_torch.py +112 -0
  9. moospread/utils/__init__.py +8 -0
  10. moospread/utils/constraint_utils/__init__.py +2 -0
  11. moospread/utils/constraint_utils/gradient.py +72 -0
  12. moospread/utils/constraint_utils/mgda_core.py +69 -0
  13. moospread/utils/constraint_utils/pmgda_solver.py +308 -0
  14. moospread/utils/constraint_utils/prefs.py +64 -0
  15. moospread/utils/ditmoo.py +127 -0
  16. moospread/utils/lhs.py +74 -0
  17. moospread/utils/misc.py +28 -0
  18. moospread/utils/mobo_utils/__init__.py +11 -0
  19. moospread/utils/mobo_utils/evolution/__init__.py +0 -0
  20. moospread/utils/mobo_utils/evolution/dom.py +60 -0
  21. moospread/utils/mobo_utils/evolution/norm.py +40 -0
  22. moospread/utils/mobo_utils/evolution/utils.py +97 -0
  23. moospread/utils/mobo_utils/learning/__init__.py +0 -0
  24. moospread/utils/mobo_utils/learning/model.py +40 -0
  25. moospread/utils/mobo_utils/learning/model_init.py +33 -0
  26. moospread/utils/mobo_utils/learning/model_update.py +51 -0
  27. moospread/utils/mobo_utils/learning/prediction.py +116 -0
  28. moospread/utils/mobo_utils/learning/utils.py +143 -0
  29. moospread/utils/mobo_utils/lhs_for_mobo.py +243 -0
  30. moospread/utils/mobo_utils/mobo/__init__.py +0 -0
  31. moospread/utils/mobo_utils/mobo/acquisition.py +209 -0
  32. moospread/utils/mobo_utils/mobo/algorithms.py +91 -0
  33. moospread/utils/mobo_utils/mobo/factory.py +86 -0
  34. moospread/utils/mobo_utils/mobo/mobo.py +132 -0
  35. moospread/utils/mobo_utils/mobo/selection.py +182 -0
  36. moospread/utils/mobo_utils/mobo/solver/__init__.py +5 -0
  37. moospread/utils/mobo_utils/mobo/solver/moead.py +17 -0
  38. moospread/utils/mobo_utils/mobo/solver/nsga2.py +10 -0
  39. moospread/utils/mobo_utils/mobo/solver/parego/__init__.py +1 -0
  40. moospread/utils/mobo_utils/mobo/solver/parego/parego.py +62 -0
  41. moospread/utils/mobo_utils/mobo/solver/parego/utils.py +34 -0
  42. moospread/utils/mobo_utils/mobo/solver/pareto_discovery/__init__.py +1 -0
  43. moospread/utils/mobo_utils/mobo/solver/pareto_discovery/buffer.py +364 -0
  44. moospread/utils/mobo_utils/mobo/solver/pareto_discovery/pareto_discovery.py +571 -0
  45. moospread/utils/mobo_utils/mobo/solver/pareto_discovery/utils.py +168 -0
  46. moospread/utils/mobo_utils/mobo/solver/solver.py +74 -0
  47. moospread/utils/mobo_utils/mobo/surrogate_model/__init__.py +2 -0
  48. moospread/utils/mobo_utils/mobo/surrogate_model/base.py +36 -0
  49. moospread/utils/mobo_utils/mobo/surrogate_model/gaussian_process.py +177 -0
  50. moospread/utils/mobo_utils/mobo/surrogate_model/thompson_sampling.py +79 -0
  51. moospread/utils/mobo_utils/mobo/surrogate_problem.py +44 -0
  52. moospread/utils/mobo_utils/mobo/transformation.py +106 -0
  53. moospread/utils/mobo_utils/mobo/utils.py +65 -0
  54. moospread/utils/mobo_utils/spread_mobo_utils.py +854 -0
  55. moospread/utils/offline_utils/__init__.py +10 -0
  56. moospread/utils/offline_utils/handle_task.py +203 -0
  57. moospread/utils/offline_utils/proxies.py +338 -0
  58. moospread/utils/spread_utils.py +91 -0
  59. moospread-0.1.0.dist-info/METADATA +75 -0
  60. moospread-0.1.0.dist-info/RECORD +63 -0
  61. moospread-0.1.0.dist-info/WHEEL +5 -0
  62. moospread-0.1.0.dist-info/licenses/LICENSE +10 -0
  63. moospread-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,571 @@
1
+ import numpy as np
2
+ from scipy.optimize import minimize
3
+ from scipy.linalg import null_space
4
+ from pymoo.model.algorithm import Algorithm
5
+ from pymoo.model.duplicate import DefaultDuplicateElimination
6
+ from pymoo.model.individual import Individual
7
+ from pymoo.model.initialization import Initialization
8
+ from multiprocessing import Process, Queue, cpu_count
9
+ import sys
10
+
11
+ from moospread.utils.mobo_utils.mobo.solver.pareto_discovery.buffer import get_buffer
12
+ from moospread.utils.mobo_utils.mobo.solver.pareto_discovery.utils import propose_next_batch, propose_next_batch_without_label, get_sample_num_from_families
13
+ from moospread.utils.mobo_utils.mobo.solver.solver import Solver
14
+
15
+
16
+ def _local_optimization(x, y, f, eval_func, bounds, delta_s):
17
+ '''
18
+ Local optimization of generated stochastic samples by minimizing distance to the target, see section 6.2.3.
19
+ Input:
20
+ x: a design sample, shape = (n_var,)
21
+ y: performance of x, shape = (n_obj,)
22
+ f: relative performance to the buffer origin, shape = (n_obj,)
23
+ eval_func: problem's evaluation function
24
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
25
+ delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
26
+ Output:
27
+ x_opt: locally optimized sample x
28
+ '''
29
+ # choose reference point z
30
+ f_norm = np.linalg.norm(f)
31
+ s = 2.0 * f / np.sum(f) - 1 - f / f_norm
32
+ s /= np.linalg.norm(s)
33
+ z = y + s * delta_s * np.linalg.norm(f)
34
+
35
+ # optimization objective, see eq(4)
36
+ def fun(x):
37
+ fx = eval_func(x, return_values_of=['F'])
38
+ return np.linalg.norm(fx - z)
39
+
40
+ # jacobian of the objective
41
+ dy = eval_func(x, return_values_of=['dF'])
42
+ if dy is None:
43
+ jac = None
44
+ else:
45
+ def jac(x):
46
+ fx, dfx = eval_func(x, return_values_of=['F', 'dF'])
47
+ return ((fx - z) / np.linalg.norm(fx - z)) @ dfx
48
+
49
+ # do optimization using LBFGS
50
+ res = minimize(fun, x, method='L-BFGS-B', jac=jac, bounds=np.array(bounds).T)
51
+ x_opt = res.x
52
+ return x_opt
53
+
54
+
55
+ def _get_kkt_dual_variables(F, G, DF, DG):
56
+ '''
57
+ Optimizing for dual variables alpha and beta in KKT conditions, see section 4.2, proposition 4.5.
58
+ Input:
59
+ Given a design sample,
60
+ F: performance value, shape = (n_obj,)
61
+ G: active constraints, shape = (n_active_const,)
62
+ DF: jacobian matrix of performance, shape = (n_obj, n_var)
63
+ DG: jacobian matrix of active constraints, shape = (n_active_const, n_var)
64
+ where n_var = D, n_obj = d, n_active_const = K' in the original paper
65
+ Output:
66
+ alpha_opt, beta_opt: optimized dual variables
67
+ '''
68
+ # NOTE: use min-norm solution for solving alpha then determine beta instead?
69
+ n_obj = len(F)
70
+ n_active_const = len(G) if G is not None else 0
71
+
72
+ '''
73
+ Optimization formulation:
74
+ To optimize the last line of (2) in section 4.2, we change it to a quadratic optization problem by:
75
+ find x to let Ax = 0 --> min_x (Ax)^2
76
+ where x means [alpha, beta] and A means [DF, DG].
77
+ Constraints: alpha >= 0, beta >= 0, sum(alpha) = 1.
78
+ NOTE: we currently ignore the constraint beta * G = 0 because G will always be 0 with only box constraints, but add that constraint will result in poor optimization solution (?)
79
+ '''
80
+ if n_active_const > 0: # when there are active constraints
81
+
82
+ def fun(x, n_obj=n_obj, DF=DF, DG=DG):
83
+ alpha, beta = x[:n_obj], x[n_obj:]
84
+ objective = alpha @ DF + beta @ DG
85
+ return 0.5 * objective @ objective
86
+
87
+ def jac(x, n_obj=n_obj, DF=DF, DG=DG):
88
+ alpha, beta = x[:n_obj], x[n_obj:]
89
+ objective = alpha @ DF + beta @ DG
90
+ return np.vstack([DF, DG]) @ objective
91
+
92
+ const = {'type': 'eq',
93
+ 'fun': lambda x, n_obj=n_obj: np.sum(x[:n_obj]) - 1.0,
94
+ 'jac': lambda x, n_obj=n_obj: np.concatenate([np.ones(n_obj), np.zeros_like(x[n_obj:])])}
95
+
96
+ else: # when there's no active constraint
97
+
98
+ def fun(x, DF=DF):
99
+ objective = x @ DF
100
+ return 0.5 * objective @ objective
101
+
102
+ def jac(x, DF=DF):
103
+ objective = x @ DF
104
+ return DF @ objective
105
+
106
+ const = {'type': 'eq',
107
+ 'fun': lambda x: np.sum(x) - 1.0,
108
+ 'jac': np.ones_like}
109
+
110
+ # specify different bounds for alpha and beta
111
+ bounds = np.array([[0.0, np.inf]] * (n_obj + n_active_const))
112
+
113
+ # NOTE: we use random value to initialize alpha for now, maybe consider the location of F we can get a more accurate initialization
114
+ alpha_init = np.random.random(len(F))
115
+ alpha_init /= np.sum(alpha_init)
116
+ beta_init = np.zeros(n_active_const) # zero initialization for beta
117
+ x_init = np.concatenate([alpha_init, beta_init])
118
+
119
+ # do optimization using SLSQP
120
+ res = minimize(fun, x_init, method='SLSQP', jac=jac, bounds=bounds, constraints=const)
121
+ x_opt = res.x
122
+ alpha_opt, beta_opt = x_opt[:n_obj], x_opt[n_obj:]
123
+ return alpha_opt, beta_opt
124
+
125
+
126
+ def _get_active_box_const(x, bounds):
127
+ '''
128
+ Getting the indices of active box constraints.
129
+ Input:
130
+ x: a design sample, shape = (n_var,)
131
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
132
+ Output:
133
+ active_idx: indices of all active constraints
134
+ upper_active_idx: indices of upper active constraints
135
+ lower_active_idx: indices of lower active constraints
136
+ '''
137
+ eps = 1e-8 # epsilon value to determine 'active'
138
+ upper_active = bounds[1] - x < eps
139
+ lower_active = x - bounds[0] < eps
140
+ active = np.logical_or(upper_active, lower_active)
141
+ active_idx, upper_active_idx, lower_active_idx = np.where(active)[0], np.where(upper_active)[0], np.where(lower_active)[0]
142
+ return active_idx, upper_active_idx, lower_active_idx
143
+
144
+
145
+ def _get_box_const_value_jacobian_hessian(x, bounds):
146
+ '''
147
+ Getting the value, jacobian and hessian of active box constraints.
148
+ Input:
149
+ x: a design sample, shape = (n_var,)
150
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
151
+ Output:
152
+ G: value of active box constraints (always 0), shape = (n_active_const,)
153
+ DG: jacobian matrix of active box constraints (1/-1 at active locations, otherwise 0), shape = (n_active_const, n_var)
154
+ HG: hessian matrix of active box constraints (always 0), shape = (n_active_const, n_var, n_var)
155
+ '''
156
+ # get indices of active constraints
157
+ active_idx, upper_active_idx, _ = _get_active_box_const(x, bounds)
158
+ n_active_const, n_var = len(active_idx), len(x)
159
+
160
+ if n_active_const > 0:
161
+ G = np.zeros(n_active_const)
162
+ DG = np.zeros((n_active_const, n_var))
163
+ for i, idx in enumerate(active_idx):
164
+ constraint = np.zeros(n_var)
165
+ if idx in upper_active_idx:
166
+ constraint[idx] = 1 # upper active
167
+ else:
168
+ constraint[idx] = -1 # lower active
169
+ DG[i] = constraint
170
+ HG = np.zeros((n_active_const, n_var, n_var))
171
+ return G, DG, HG
172
+ else:
173
+ # no active constraints
174
+ return None, None, None
175
+
176
+
177
+ def _get_optimization_directions(x_opt, eval_func, bounds):
178
+ '''
179
+ Getting the directions to explore local pareto manifold.
180
+ Input:
181
+ x_opt: locally optimized design sample, shape = (n_var,)
182
+ eval_func: problem's evaluation function
183
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
184
+ Output:
185
+ directions: local exploration directions for alpha, beta and x (design sample)
186
+ '''
187
+ # evaluate the value, jacobian and hessian of performance
188
+ F, DF, HF = eval_func(x_opt, return_values_of=['F', 'dF', 'hF'])
189
+
190
+ # evaluate the value, jacobian and hessian of box constraint (NOTE: assume no other types of constraints)
191
+ G, DG, HG = _get_box_const_value_jacobian_hessian(x_opt, bounds)
192
+
193
+ # KKT dual variables optimization
194
+ alpha, beta = _get_kkt_dual_variables(F, G, DF, DG)
195
+
196
+ n_obj, n_var, n_active_const = len(F), len(x_opt), len(G) if G is not None else 0
197
+
198
+ # compute H in eq(3) (NOTE: the two forms below are equivalent for box constraint since HG = 0)
199
+ if n_active_const > 0:
200
+ H = HF.T @ alpha + HG.T @ beta
201
+ else:
202
+ H = HF.T @ alpha
203
+
204
+ # compute exploration directions (unnormalized) by taking the null space of image in eq(3)
205
+ # TODO: this part is mainly copied from Adriana's implementation, to be checked
206
+ # NOTE: seems useless to solve for d_alpha and d_beta, maybe need to consider all possible situations in null_space computation
207
+ alpha_const = np.concatenate([np.ones(n_obj), np.zeros(n_active_const + n_var)])
208
+ if n_active_const > 0:
209
+ comp_slack_const = np.column_stack([np.zeros((n_active_const, n_obj + n_active_const)), DG])
210
+ DxHx = np.vstack([alpha_const, comp_slack_const, np.column_stack([DF.T, DG.T, H])])
211
+ else:
212
+ DxHx = np.vstack([alpha_const, np.column_stack([DF.T, H])])
213
+ directions = null_space(DxHx)
214
+
215
+ # eliminate numerical error
216
+ eps = 1e-8
217
+ directions[np.abs(directions) < eps] = 0.0
218
+ return directions
219
+
220
+
221
+ def _first_order_approximation(x_opt, directions, bounds, n_grid_sample):
222
+ '''
223
+ Exploring new samples from local manifold (first order approximation of pareto front).
224
+ Input:
225
+ x_opt: locally optimized design sample, shape = (n_var,)
226
+ directions: local exploration directions for alpha, beta and x (design sample)
227
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
228
+ n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
229
+ Output:
230
+ x_samples: new valid samples from local manifold (grid)
231
+ '''
232
+ n_var = len(x_opt)
233
+ lower_bound, upper_bound = bounds[0], bounds[1]
234
+ active_idx, _, _ = _get_active_box_const(x_opt, bounds)
235
+ n_active_const = len(active_idx)
236
+ n_obj = len(directions) - n_var - n_active_const
237
+
238
+ x_samples = np.array([x_opt])
239
+
240
+ # TODO: check why unused d_alpha and d_beta here
241
+ d_alpha, d_beta, d_x = directions[:n_obj], directions[n_obj:n_obj + n_active_const], directions[-n_var:]
242
+ eps = 1e-8
243
+ if np.linalg.norm(d_x) < eps: # direction is a zero vector
244
+ return x_samples
245
+ direction_dim = d_x.shape[1]
246
+
247
+ if direction_dim > n_obj - 1:
248
+ # more than d-1 directions to explore, randomly choose d-1 sub-directions
249
+ indices = np.random.choice(np.arange(direction_dim), n_obj - 1)
250
+ while np.linalg.norm(d_x[:, indices]) < eps:
251
+ indices = np.random.choice(np.arange(direction_dim), n_obj - 1)
252
+ d_x = d_x[:, indices]
253
+ elif direction_dim < n_obj - 1:
254
+ # less than d-1 directions to explore, do not expand the point
255
+ return x_samples
256
+
257
+ # normalize direction
258
+ d_x /= np.linalg.norm(d_x)
259
+
260
+ # NOTE: Adriana's code also checks if such direction has been expanded, but maybe unnecessary
261
+
262
+ # grid sampling on expanded surface (NOTE: more delicate sampling scheme?)
263
+ bound_scale = np.expand_dims(upper_bound - lower_bound, axis=1)
264
+ d_x *= bound_scale
265
+ loop_count = 0 # avoid infinite loop when it's hard to get valid samples
266
+ while len(x_samples) < n_grid_sample:
267
+ # compute expanded samples
268
+ curr_dx_samples = np.sum(np.expand_dims(d_x, axis=0) * np.random.random((n_grid_sample, 1, n_obj - 1)), axis=-1)
269
+ curr_x_samples = np.expand_dims(x_opt, axis=0) + curr_dx_samples
270
+ # check validity of samples
271
+ valid_idx = np.where(np.logical_and((curr_x_samples <= upper_bound).all(axis=1), (curr_x_samples >= lower_bound).all(axis=1)))[0]
272
+ x_samples = np.vstack([x_samples, curr_x_samples[valid_idx]])
273
+ loop_count += 1
274
+ if loop_count > 10:
275
+ break
276
+ x_samples = x_samples[:n_grid_sample]
277
+ return x_samples
278
+
279
+
280
+ def _pareto_discover(xs, eval_func, bounds, delta_s, origin, origin_constant, n_grid_sample, queue):
281
+ '''
282
+ Local optimization and first-order approximation.
283
+ (We move these functions out from the ParetoDiscovery class for parallelization)
284
+ Input:
285
+ xs: a batch of samples x, shape = (batch_size, n_var)
286
+ eval_func: problem's evaluation function
287
+ bounds: problem's lower and upper bounds, shape = (2, n_var)
288
+ delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
289
+ origin: origin of performance buffer
290
+ origin_constant: when evaluted value surpasses the buffer origin, adjust the origin accordingly and subtract this constant
291
+ n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
292
+ queue: the queue storing results from all processes
293
+ Output (stored in queue):
294
+ x_samples_all: all valid samples from local manifold (grid)
295
+ patch_ids: patch ids for all valid samples (same id when expanded from same x)
296
+ sample_num: number of input samples (needed for counting global patch ids)
297
+ new_origin: new origin point for performance buffer
298
+ '''
299
+ # evaluate samples x and adjust origin accordingly
300
+ ys = eval_func(xs, return_values_of=['F'])
301
+ new_origin = np.minimum(origin, np.min(ys, axis=0))
302
+ if (new_origin != origin).any():
303
+ new_origin -= origin_constant
304
+ fs = ys - new_origin
305
+
306
+ x_samples_all = []
307
+ patch_ids = []
308
+ for i, (x, y, f) in enumerate(zip(xs, ys, fs)):
309
+
310
+ # local optimization by optimizing eq(4)
311
+ x_opt = _local_optimization(x, y, f, eval_func, bounds, delta_s)
312
+
313
+ # get directions to expand in local manifold
314
+ directions = _get_optimization_directions(x_opt, eval_func, bounds)
315
+
316
+ # get new valid samples from local manifold
317
+ x_samples = _first_order_approximation(x_opt, directions, bounds, n_grid_sample)
318
+ x_samples_all.append(x_samples)
319
+ patch_ids.extend([i] * len(x_samples))
320
+
321
+ queue.put([np.vstack(x_samples_all), patch_ids, len(xs), new_origin])
322
+
323
+
324
+ class ParetoDiscovery(Algorithm):
325
+ '''
326
+ The Pareto discovery algorithm introduced by: Schulz, Adriana, et al. "Interactive exploration of design trade-offs." ACM Transactions on Graphics (TOG) 37.4 (2018): 1-14.
327
+ '''
328
+ def __init__(self,
329
+ pop_size=None,
330
+ sampling=None,
331
+ survival=None,
332
+ eliminate_duplicates=DefaultDuplicateElimination(),
333
+ repair=None,
334
+ individual=Individual(),
335
+ n_cell=None,
336
+ cell_size=None,
337
+ buffer_origin=None,
338
+ buffer_origin_constant=1e-2,
339
+ delta_b=0.2,
340
+ label_cost=0,
341
+ delta_p=10,
342
+ delta_s=0.3,
343
+ n_grid_sample=1000,
344
+ n_process=cpu_count(),
345
+ **kwargs
346
+ ):
347
+ '''
348
+ Inputs (essential parameters):
349
+ pop_size: population size
350
+ sampling: initial sample data or sampling method to obtain initial population
351
+ n_cell: number of cells in performance buffer
352
+ cell_size: maximum number of samples inside each cell of performance buffer
353
+ buffer_origin: origin of performance buffer
354
+ buffer_origin_constant: when evaluted value surpasses the buffer origin, adjust the origin accordingly and subtract this constant
355
+ delta_b: unary energy normalization constant for sparse approximation, see section 6.4
356
+ label_cost: for reducing number of unique labels in sparse approximation, see section 6.4
357
+ delta_p: factor of perturbation in stochastic sampling, see section 6.2.2
358
+ delta_s: scaling factor for choosing reference point in local optimization, see section 6.2.3
359
+ n_grid_sample: number of samples on local manifold (grid), see section 6.3.1
360
+ n_process: number of processes for parallelization
361
+ '''
362
+ super().__init__(**kwargs)
363
+
364
+ self.pop_size = pop_size
365
+ self.survival = survival
366
+ self.individual = individual
367
+
368
+ if isinstance(eliminate_duplicates, bool):
369
+ if eliminate_duplicates:
370
+ self.eliminate_duplicates = DefaultDuplicateElimination()
371
+ else:
372
+ self.eliminate_duplicates = None
373
+ else:
374
+ self.eliminate_duplicates = eliminate_duplicates
375
+
376
+ self.initialization = Initialization(sampling,
377
+ individual=individual,
378
+ repair=repair,
379
+ eliminate_duplicates=self.eliminate_duplicates)
380
+
381
+ self.n_gen = None
382
+ self.pop = None
383
+ self.off = None
384
+
385
+ self.approx_set = None
386
+ self.approx_front = None
387
+ self.fam_lbls = None
388
+
389
+ self.buffer = None
390
+ if n_cell is None:
391
+ n_cell = self.pop_size
392
+ self.buffer_args = {'cell_num': n_cell,
393
+ 'cell_size': cell_size,
394
+ 'origin': buffer_origin,
395
+ 'origin_constant': buffer_origin_constant,
396
+ 'delta_b': delta_b,
397
+ 'label_cost': label_cost}
398
+
399
+ self.delta_p = delta_p
400
+ self.delta_s = delta_s
401
+ self.n_grid_sample = n_grid_sample
402
+ self.n_process = n_process
403
+ self.patch_id = 0
404
+
405
+ def _initialize(self):
406
+ # create the initial population
407
+ pop = self.initialization.do(self.problem, self.pop_size, algorithm=self)
408
+ pop_x = pop.get('X').copy()
409
+ pop_f = self.problem.evaluate(pop_x, return_values_of=['F'])
410
+
411
+ # initialize buffer
412
+ self.buffer = get_buffer(self.problem.n_obj, **self.buffer_args)
413
+ self.buffer.origin = self.problem.transformation.do(y=self.buffer.origin)
414
+ patch_ids = np.full(self.pop_size, self.patch_id) # NOTE: patch_ids here might not make sense
415
+ self.patch_id += 1
416
+ self.buffer.insert(pop_x, pop_f, patch_ids)
417
+
418
+ # update population by the best samples in the buffer
419
+ pop = pop.new('X', self.buffer.sample(self.pop_size))
420
+
421
+ # evaluate population using the objective function
422
+ self.evaluator.eval(self.problem, pop, algorithm=self)
423
+
424
+ # NOTE: check if need survival here
425
+ if self.survival:
426
+ pop = self.survival.do(self.problem, pop, len(pop), algorithm=self)
427
+
428
+ self.pop = pop
429
+
430
+ sys.stdout.write('ParetoDiscovery optimizing: generation %i' % self.n_gen)
431
+ sys.stdout.flush()
432
+
433
+ def _next(self):
434
+ '''
435
+ Core algorithm part in each iteration, see algorithm 1.
436
+ --------------------------------------
437
+ xs = stochastic_sampling(B, F, X)
438
+ for x in xs:
439
+ D = select_direction(B, x)
440
+ x_opt = local_optimization(D, F, X)
441
+ M = first_order_approximation(x_opt, F, X)
442
+ update_buffer(B, F(M))
443
+ --------------------------------------
444
+ where F is problem evaluation, X is design constraints
445
+ '''
446
+ # update optimization progress
447
+ sys.stdout.write('\b' * len(str(self.n_gen - 1)) + str(self.n_gen))
448
+ sys.stdout.flush()
449
+
450
+ # stochastic sampling by adding local perturbance
451
+ xs = self._stochastic_sampling()
452
+
453
+ # parallelize core pareto discovery process by multiprocessing, see _pareto_discover()
454
+ # including select_direction, local_optimization, first_order_approximation in above algorithm illustration
455
+ x_batch = np.array_split(xs, self.n_process)
456
+ queue = Queue()
457
+ process_count = 0
458
+ for x in x_batch:
459
+ if len(x) > 0:
460
+ p = Process(target=_pareto_discover,
461
+ args=(x, self.problem.evaluate, [self.problem.xl, self.problem.xu], self.delta_s,
462
+ self.buffer.origin, self.buffer.origin_constant, self.n_grid_sample, queue))
463
+ p.start()
464
+ process_count += 1
465
+
466
+ # gather results (new samples, new patch ids, new origin of performance buffer) from parallel discovery
467
+ new_origin = self.buffer.origin
468
+ x_samples_all = []
469
+ patch_ids_all = []
470
+ for _ in range(process_count):
471
+ x_samples, patch_ids, sample_num, origin = queue.get()
472
+ if x_samples is not None:
473
+ x_samples_all.append(x_samples)
474
+ patch_ids_all.append(np.array(patch_ids) + self.patch_id) # assign corresponding global patch ids to samples
475
+ self.patch_id += sample_num
476
+ new_origin = np.minimum(new_origin, origin)
477
+
478
+ # evalaute all new samples and adjust the origin point of buffer
479
+ x_samples_all = np.vstack(x_samples_all)
480
+ y_samples_all = self.problem.evaluate(x_samples_all, return_values_of=['F'])
481
+ new_origin = np.minimum(np.min(y_samples_all, axis=0), new_origin)
482
+ patch_ids_all = np.concatenate(patch_ids_all)
483
+
484
+ # update buffer
485
+ self.buffer.move_origin(new_origin)
486
+ self.buffer.insert(x_samples_all, y_samples_all, patch_ids_all)
487
+
488
+ # update population by the best samples in the buffer
489
+ self.pop = self.pop.new('X', self.buffer.sample(self.pop_size))
490
+ self.evaluator.eval(self.problem, self.pop, algorithm=self)
491
+
492
+ def _stochastic_sampling(self):
493
+ '''
494
+ Stochastic sampling around current population to initialize each iteration to avoid local minima, see section 6.2.2
495
+ '''
496
+ xs = self.pop.get('X').copy()
497
+
498
+ # generate stochastic direction
499
+ d = np.random.random(xs.shape)
500
+ d /= np.expand_dims(np.linalg.norm(d, axis=1), axis=1)
501
+
502
+ # generate random scaling factor
503
+ delta = np.random.random() * self.delta_p
504
+
505
+ # generate new stochastic samples
506
+ xs = xs + 1.0 / (2 ** delta) * d # NOTE: is this scaling form reasonable? maybe better use relative scaling?
507
+ xs = np.clip(xs, self.problem.xl, self.problem.xu)
508
+ return xs
509
+
510
+ def propose_next_batch(self, curr_pfront, ref_point, batch_size, transformation):
511
+ '''
512
+ Propose next batch to evaluate for active learning.
513
+ Greedely propose sample with max HV until all families ar visited. Allow only samples with max HV from unvisited family.
514
+ '''
515
+ approx_x, approx_y = transformation.undo(self.approx_set, self.approx_front)
516
+ labels = self.fam_lbls
517
+
518
+ X_next = []
519
+ Y_next = []
520
+ family_lbls = []
521
+
522
+ if len(approx_x) >= batch_size:
523
+ # approximation result is enough to propose all candidates
524
+ curr_X_next, curr_Y_next, labels_next = propose_next_batch(curr_pfront, ref_point, approx_y, approx_x, batch_size, labels)
525
+ X_next.append(curr_X_next)
526
+ Y_next.append(curr_Y_next)
527
+ family_lbls.append(labels_next)
528
+
529
+ else:
530
+ # approximation result is not enough to propose all candidates
531
+ # so propose all result as candidates, and propose others from buffer
532
+ # NOTE: may consider re-expanding manifolds to produce more approximation result, but may not be necessary
533
+ X_next.append(approx_x)
534
+ Y_next.append(approx_y)
535
+ family_lbls.extend(labels)
536
+ remain_batch_size = batch_size - len(approx_x)
537
+ buffer_xs, buffer_ys = self.buffer.flattened()
538
+ buffer_xs, buffer_ys = transformation.undo(buffer_xs, buffer_ys)
539
+ prop_X_next, prop_Y_next = propose_next_batch_without_label(curr_pfront, ref_point, buffer_ys, buffer_xs, remain_batch_size)
540
+ X_next.append(prop_X_next)
541
+ Y_next.append(prop_Y_next)
542
+ family_lbls.extend(np.full(remain_batch_size, -1))
543
+
544
+ X_next = np.vstack(X_next)
545
+ Y_next = np.vstack(Y_next)
546
+ return X_next, Y_next, family_lbls
547
+
548
+ def get_sparse_front(self, transformation):
549
+ '''
550
+ Get sparse approximation of Pareto front and set
551
+ '''
552
+ approx_x, approx_y = transformation.undo(self.approx_set, self.approx_front)
553
+ labels = self.fam_lbls
554
+
555
+ return labels, approx_x, approx_y
556
+
557
+ def _finalize(self):
558
+ # set population as all samples in performance buffer
559
+ pop_x, pop_y = self.buffer.flattened()
560
+ self.pop = self.pop.new('X', pop_x, 'F', pop_y)
561
+ # get sparse front approximation
562
+ self.fam_lbls, self.approx_set, self.approx_front = self.buffer.sparse_approximation()
563
+ print()
564
+
565
+
566
+ class ParetoDiscoverySolver(Solver):
567
+ '''
568
+ Solver based on ParetoDiscovery
569
+ '''
570
+ def __init__(self, *args, **kwargs):
571
+ super().__init__(*args, algo=ParetoDiscovery, **kwargs)