|
| 1 | +import numpy as np |
| 2 | +import traceback |
| 3 | + |
| 4 | +from openmdao.core.driver import Driver, RecordingDebugging |
| 5 | +from openmdao.core.analysis_error import AnalysisError |
| 6 | + |
| 7 | +EGOBOX_NOT_INSTALLED = False |
| 8 | +try: |
| 9 | + import egobox as egx |
| 10 | + from egobox import Egor |
| 11 | +except ImportError: |
| 12 | + EGOBOX_NOT_INSTALLED = True |
| 13 | + |
| 14 | + |
| 15 | +def to_list(l, size): |
| 16 | + if not (isinstance(l, np.ndarray) or isinstance(l, list)): |
| 17 | + return [l] * size |
| 18 | + diff_len = len(l) - size |
| 19 | + if diff_len > 0: |
| 20 | + return l[0:size] |
| 21 | + elif diff_len < 0: |
| 22 | + return [l[0]] * size |
| 23 | + else: |
| 24 | + return l |
| 25 | + |
| 26 | + |
| 27 | +class EgoboxEgorDriver(Driver): |
| 28 | + """OpenMDAO driver for egobox optimizer""" |
| 29 | + |
| 30 | + def __init__(self, **kwargs): |
| 31 | + """Initialize the driver with the given options.""" |
| 32 | + super(EgoboxEgorDriver, self).__init__(**kwargs) |
| 33 | + |
| 34 | + if EGOBOX_NOT_INSTALLED: |
| 35 | + raise RuntimeError("egobox library is not installed.") |
| 36 | + |
| 37 | + # What we support |
| 38 | + self.supports["inequality_constraints"] = True |
| 39 | + self.supports["linear_constraints"] = True |
| 40 | + |
| 41 | + # What we don't support |
| 42 | + self.supports["equality_constraints"] = False |
| 43 | + self.supports["two_sided_constraints"] = False |
| 44 | + self.supports["multiple_objectives"] = False |
| 45 | + self.supports["active_set"] = False |
| 46 | + self.supports["simultaneous_derivatives"] = False |
| 47 | + self.supports["total_jac_sparsity"] = False |
| 48 | + self.supports["gradients"] = False |
| 49 | + self.supports["integer_design_vars"] = False |
| 50 | + |
| 51 | + self.opt_settings = {} |
| 52 | + |
| 53 | + def _declare_options(self): |
| 54 | + self.options.declare( |
| 55 | + "optimizer", |
| 56 | + default="EGOR", |
| 57 | + values=["EGOR"], |
| 58 | + desc="Name of optimizer to use", |
| 59 | + ) |
| 60 | + |
| 61 | + def _setup_driver(self, problem): |
| 62 | + super(EgoboxEgorDriver, self)._setup_driver(problem) |
| 63 | + |
| 64 | + self.comm = None |
| 65 | + |
| 66 | + def run(self): |
| 67 | + model = self._problem().model |
| 68 | + |
| 69 | + self.iter_count = 0 |
| 70 | + self.name = "egobox_optimizer_egor" |
| 71 | + |
| 72 | + # Initial Run |
| 73 | + with RecordingDebugging( |
| 74 | + self.options["optimizer"], self.iter_count, self |
| 75 | + ) as rec: |
| 76 | + # Initial Run |
| 77 | + model._solve_nonlinear() |
| 78 | + rec.abs = 0.0 |
| 79 | + rec.rel = 0.0 |
| 80 | + self.iter_count += 1 |
| 81 | + |
| 82 | + # Format design variables to suit segomoe implementation |
| 83 | + self.xspecs = self._initialize_vars() |
| 84 | + |
| 85 | + # Format constraints to suit segomoe implementation |
| 86 | + self.n_cstr = self._initialize_cons() |
| 87 | + |
| 88 | + # Format option dictionary to suit Egor implementation |
| 89 | + optim_settings = { |
| 90 | + "cstr_tol": 1e-6, |
| 91 | + } |
| 92 | + n_iter = self.opt_settings["maxiter"] |
| 93 | + optim_settings.update( |
| 94 | + {k: v for k, v in self.opt_settings.items() if k != "maxiter"} |
| 95 | + ) |
| 96 | + |
| 97 | + dim = 0 |
| 98 | + for name, meta in self._designvars.items(): |
| 99 | + dim += meta["size"] |
| 100 | + print("Designvars dimension: ", dim) |
| 101 | + if dim > 10: |
| 102 | + self.optim_settings["kpls_dim"] = 3 |
| 103 | + |
| 104 | + # Instanciate a SEGO optimizer |
| 105 | + egor = Egor( |
| 106 | + self._objfunc, |
| 107 | + xspecs=self.xspecs, |
| 108 | + n_cstr=self.n_cstr, |
| 109 | + **optim_settings, |
| 110 | + ) |
| 111 | + |
| 112 | + # Run the optim |
| 113 | + res = egor.minimize(n_eval=n_iter) |
| 114 | + |
| 115 | + # Set optimal parameters |
| 116 | + i = 0 |
| 117 | + for name, meta in self._designvars.items(): |
| 118 | + size = meta["size"] |
| 119 | + self.set_design_var(name, res.x_opt[i : i + size]) |
| 120 | + i += size |
| 121 | + |
| 122 | + with RecordingDebugging( |
| 123 | + self.options["optimizer"], self.iter_count, self |
| 124 | + ) as rec: |
| 125 | + model._solve_nonlinear() |
| 126 | + rec.abs = 0.0 |
| 127 | + rec.rel = 0.0 |
| 128 | + self.iter_count += 1 |
| 129 | + |
| 130 | + return True |
| 131 | + |
| 132 | + def _initialize_vars(self): |
| 133 | + variables = [] |
| 134 | + desvars = self._designvars |
| 135 | + for _, meta in desvars.items(): |
| 136 | + if meta["size"] > 1: |
| 137 | + if np.isscalar(meta["lower"]): |
| 138 | + variables += [ |
| 139 | + egx.Vspec( |
| 140 | + egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]] |
| 141 | + ) |
| 142 | + for i in range(meta["size"]) |
| 143 | + ] |
| 144 | + else: |
| 145 | + variables += [ |
| 146 | + egx.Vspec( |
| 147 | + egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]] |
| 148 | + ) |
| 149 | + for i in range(meta["size"]) |
| 150 | + ] |
| 151 | + else: |
| 152 | + variables += [ |
| 153 | + egx.Vspec( |
| 154 | + egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]] |
| 155 | + ) |
| 156 | + ] |
| 157 | + return variables |
| 158 | + |
| 159 | + def _initialize_cons(self, eq_tol=None, ieq_tol=None): |
| 160 | + """Format OpenMDAO constraints to suit EGOR implementation |
| 161 | +
|
| 162 | + Parameters |
| 163 | + ---------- |
| 164 | + eq_tol: dict |
| 165 | + Dictionary to define specific tolerance for eq constraints |
| 166 | + {'[groupName]': [tol]} Default tol = 1e-5 |
| 167 | + """ |
| 168 | + con_meta = self._cons |
| 169 | + |
| 170 | + self.ieq_cons = { |
| 171 | + name: con for name, con in con_meta.items() if not con["equals"] |
| 172 | + } |
| 173 | + |
| 174 | + # Inequality constraints |
| 175 | + n_cstr = 0 |
| 176 | + for name in self.ieq_cons.keys(): |
| 177 | + meta = con_meta[name] |
| 178 | + size = meta["size"] |
| 179 | + # Bounds - double sided is supported |
| 180 | + lower = to_list(meta["lower"], size) |
| 181 | + upper = to_list(meta["upper"], size) |
| 182 | + for k in range(size): |
| 183 | + if (lower[k] is None or lower[k] < -1e29) and upper[k] == 0.0: |
| 184 | + n_cstr += 1 |
| 185 | + else: |
| 186 | + raise ValueError( |
| 187 | + f"Constraint {lower[k]} < g(x) < {upper[k]} not handled by Egor driver" |
| 188 | + ) |
| 189 | + return n_cstr |
| 190 | + |
| 191 | + def _objfunc(self, points): |
| 192 | + """ |
| 193 | + Function that evaluates and returns the objective function and the |
| 194 | + constraints. This function is called by SEGOMOE |
| 195 | +
|
| 196 | + Parameters |
| 197 | + ---------- |
| 198 | + point : numpy.ndarray |
| 199 | + point to evaluate |
| 200 | +
|
| 201 | + Returns |
| 202 | + ------- |
| 203 | + func_dict : dict |
| 204 | + Dictionary of all functional variables evaluated at design point. |
| 205 | + fail : int |
| 206 | + 0 for successful function evaluation |
| 207 | + 1 for unsuccessful function evaluation |
| 208 | + """ |
| 209 | + res = np.zeros((points.shape[0], 1 + self.n_cstr)) |
| 210 | + model = self._problem().model |
| 211 | + |
| 212 | + for k, point in enumerate(points): |
| 213 | + try: |
| 214 | + # Pass in new parameters |
| 215 | + i = 0 |
| 216 | + |
| 217 | + for name, meta in self._designvars.items(): |
| 218 | + size = meta["size"] |
| 219 | + self.set_design_var(name, point[i : i + size]) |
| 220 | + i += size |
| 221 | + |
| 222 | + # Execute the model |
| 223 | + with RecordingDebugging( |
| 224 | + self.options["optimizer"], self.iter_count, self |
| 225 | + ) as _: |
| 226 | + self.iter_count += 1 |
| 227 | + try: |
| 228 | + model.run_solve_nonlinear() |
| 229 | + |
| 230 | + # Let the optimizer try to handle the error |
| 231 | + except AnalysisError: |
| 232 | + model._clear_iprint() |
| 233 | + |
| 234 | + # Get the objective function evaluation - single obj support |
| 235 | + for obj in self.get_objective_values().values(): |
| 236 | + res[k, 0] = obj |
| 237 | + |
| 238 | + # Get the constraint evaluations |
| 239 | + j = 1 |
| 240 | + for con_res in self.get_constraint_values().values(): |
| 241 | + # Make sure con_res is array_like |
| 242 | + con_res = to_list(con_res, 1) |
| 243 | + # Perform mapping |
| 244 | + for i, _ in enumerate(con_res): |
| 245 | + res[k, j + i] = con_res[i] |
| 246 | + j += 1 |
| 247 | + |
| 248 | + except Exception as msg: |
| 249 | + tb = traceback.format_exc() |
| 250 | + print("Exception: %s" % str(msg)) |
| 251 | + print(70 * "=", tb, 70 * "=") |
| 252 | + |
| 253 | + return res |
0 commit comments