Source code for smt_optim.utils.multi_obj

import numpy as np
from scipy.spatial.distance import cdist

from moocore import hypervolume as moocore_hv
from pymoo.core.problem import Problem as PymooProblem


[docs] def get_pareto_mask(Y: np.ndarray) -> np.ndarray: n = Y.shape[0] is_pareto = np.ones(n, dtype=bool) for i in range(n): if not is_pareto[i]: continue # A point is dominated if another point is <= in all objectives # and strictly < in at least one dominates = np.all(Y <= Y[i], axis=1) & np.any(Y < Y[i], axis=1) # If any point dominates i -> i is not Pareto if np.any(dominates): is_pareto[i] = False return is_pareto
[docs] def get_pareto_front(Y: np.ndarray) -> np.ndarray: """ Return the non-dominated objective vectors from ``Y``. Parameters ---------- Y : np.ndarray Array of shape ``(n_samples, n_objectives)`` containing objective values for each sample. Returns ------- np.ndarray Array of shape ``(n_pareto, n_objectives)`` containing the non-dominated objective vectors (the Pareto front). Notes ----- Assumes a minimization problem for all objectives and no constraints. """ pareto_mask = get_pareto_mask(Y) pareto = Y[pareto_mask] return pareto
[docs] def get_pf_from_dataset( dataset, ctol: float = 1e-4, fid: int = -1, return_dict: bool = False ) -> np.ndarray | dict: data_dict = dataset.export_as_dict() obj = data_dict["obj"] rscv = data_dict["rscv"] fidelity = data_dict["fidelity"] feas_mask = rscv <= ctol if fid == -1: fid = np.max(fidelity) fid_mask = fidelity == fid fid_feas_mask = fid_mask & feas_mask pareto_mask = get_pareto_mask(obj[fid_feas_mask]) pareto_front = obj[fid_feas_mask][pareto_mask] if return_dict: data = { "x": data_dict["x"][fid_feas_mask][pareto_mask], "obj": pareto_front, "cstr": data_dict["cstr"][fid_feas_mask][pareto_mask], "rscv": rscv[fid_feas_mask][pareto_mask], "fidelity": fidelity[fid_feas_mask][pareto_mask], } return data return pareto_front
[docs] def hypervolume_2d(pf: np.ndarray, ref: np.ndarray) -> float: """ Compute the 2D hypervolume indicator he hypervolume of the Pareto front. Parameters ---------- pf: np.ndarray of shape (num_points, 2) Pareto front. ref: np.ndarray of shape (2, ) Reference objective values. Returns ------- float Hypervolume indicator value. Notes: Assume both objective are minimized. """ if pf.shape[1] != 2 or ref.shape[0] != 2: raise Exception( "Current hypervolume implementation is only for bi-objective optimization." ) sorted_idx = np.argsort(pf[:, 0]) sorted_pf = pf[sorted_idx] hv = 0.0 prev_f2 = ref[1] for idx in range(sorted_pf.shape[0]): f1 = sorted_pf[idx, 0] f2 = sorted_pf[idx, 1] width = ref[0] - f1 height = prev_f2 - f2 if width > 0 and height > 0: hv += width * height prev_f2 = min(prev_f2, f2) return hv
[docs] def hypervolume(pf: np.ndarray, ref: np.ndarray) -> float: """ Compute the hypervolume indicator of the Pareto front. Uses the `moocore` implementation: https://multi-objective.github.io/moocore/python/reference/generated/moocore.hypervolume.html Parameters ---------- pf: np.ndarray of shape (num_points, 2) Pareto front. ref: np.ndarray of shape (2, ) Reference objective values. Returns ------- float Hypervolume indicator value. Notes: Assume both objective are minimized. """ return moocore_hv(pf, ref=ref)
[docs] def spacing(pf: np.ndarray) -> float: """ Compute the spacing indicator of the Pareto front (Schott, 1995). A lower value is better. Parameters ---------- pf: np.ndarray of shape (num_points, num_objectives) Pareto front. Returns ------- float Spacing indicator value. Notes: Assume both objective are minimized. """ num_pf = pf.shape[0] if num_pf <= 1: return np.nan distances = cdist(pf, pf, "cityblock") np.fill_diagonal(distances, np.inf) d1 = np.min(distances, axis=1) d1_mean = np.mean(d1) value = np.sqrt(1 / (num_pf - 1) * np.sum((d1_mean - d1) ** 2)) return value
[docs] class PymooStateWrapper(PymooProblem): def __init__(self, state, scaled: bool = False, train: bool = True): """ Create a Pymoo problem instance for use with Pymoo algorithms, such as NSGA-II for multi-objective optimization. The quantities of interest are modeled using the surrogate models. Parameters ---------- state : State scaled : bool Normalize the input variables and standardize the quantities of interest (before training the surrogate models, if applicable). train : bool Train the surrogate models. Notes ----- When using `PymooStateWrapper` after a Bayesian optimization (BO) iteration, `train` should be set to `True`, as the surrogate models are not trained on the most recently evaluated infill point. """ self.state = state self.scaled = scaled self.train = train if not self.state.problem.design_space.is_all_cont: raise ValueError( "PymooStateWrapper currently requires a continuous optimization problem." ) if self.train: self.state.scale_dataset(self.scaled) self.state.build_models() prob = self.state.problem if not self.scaled: l_bounds = [] u_bounds = [] for idx, var in enumerate(prob.design_space.design_variables): l_bounds.append(var.lower) u_bounds.append(var.upper) self.l_bounds = np.array(l_bounds) self.u_bounds = np.array(u_bounds) else: self.l_bounds = np.zeros(prob.num_dim) self.u_bounds = np.ones(prob.num_dim) self.f_callables = [] self.g_callables = [] self.num_g = 0 self.h_callables = [] self.num_h = 0 ineq_bounds = [] self.num_g_lower = 0 self.num_g_upper = 0 for o_id, o_config in enumerate(prob.obj_configs): self.f_callables.append( lambda x, f=self.state.obj_models[o_id].predict_values: f(x).ravel() ) for c_id, c_config in enumerate(prob.cstr_configs): if c_config.equal is not None: self.h_callables.append( lambda x, f=self.state.cstr_models[c_id].predict_values, val=c_config.equal: ( f(x).ravel() - self.state.cstr_equal[c_id] ) ) self.num_h += 1 else: self.g_callables.append( lambda x, f=self.state.cstr_models[c_id].predict_values: f( x ).ravel() ) ineq_bounds.append(np.full(2, np.nan)) if c_config.lower is not None: ineq_bounds[-1][0] = self.state.cstr_lower[c_id] self.num_g += 1 if c_config.upper is not None: ineq_bounds[-1][1] = self.state.cstr_upper[c_id] self.num_g += 1 if len(self.g_callables): self.ineq_bounds = np.array(ineq_bounds) self.g_lower_mask = np.where(np.isnan(self.ineq_bounds[:, 0]), False, True) self.g_upper_mask = np.where(np.isnan(self.ineq_bounds[:, 1]), False, True) super().__init__( n_var=prob.num_dim, n_obj=prob.num_obj, n_eq_constr=self.num_h, n_ieq_constr=self.num_g, xl=self.l_bounds, xu=self.u_bounds, ) def _evaluate(self, x, out, *args, **kwargs): num_pt = x.shape[0] x_scaled = (x - self.l_bounds) / (self.u_bounds - self.l_bounds) # sample objectives out["F"] = np.full((num_pt, self.n_obj), np.nan) for o_idx in range(self.n_obj): out["F"][:, o_idx] = self.f_callables[o_idx](x_scaled).ravel() # sample equality constraints if self.n_eq_constr > 0: out["H"] = np.empty((num_pt, self.n_eq_constr)) for h_idx in range(self.n_eq_constr): out["H"][:, h_idx] = self.h_callables[h_idx](x_scaled).ravel() if self.n_ieq_constr > 0: g_lower = np.empty((num_pt, self.ineq_bounds.shape[0])) g_upper = np.empty((num_pt, self.ineq_bounds.shape[0])) # sample inequality constraints if self.n_ieq_constr > 0: for g_idx, g_call in enumerate(self.g_callables): g_vals = self.g_callables[g_idx](x_scaled).ravel() if not np.isnan(self.ineq_bounds[g_idx, 0]): g_lower[:, g_idx] = self.ineq_bounds[g_idx, 0] - g_vals elif not np.isnan(self.ineq_bounds[g_idx, 1]): g_upper[:, g_idx] = g_vals - self.ineq_bounds[g_idx, 1] out["G"] = np.hstack( (g_lower[:, self.g_lower_mask], g_upper[:, self.g_upper_mask]) )