diff --git a/.github/environment.yml b/.github/environment.yml index 0411e61dd..577c99079 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -21,3 +21,4 @@ dependencies: # testing - parameterized - testflo + - egobox >=0.37.6, <0.38.0 diff --git a/doc/index.rst b/doc/index.rst index c9d1b0fd7..63c9dfe41 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,7 +14,7 @@ The list of supported optimizers is shown on the sidebar to the left. Of those, the following are not installed by default: - SNOPT and NLPQLP are proprietary and must be obtained from their respective authors -- IPOPT and ParOpt must be installed separately +- IPOPT, ParOpt and Egor must be installed separately pyOptSparse is a fork of `pyOpt `_. @@ -70,3 +70,4 @@ To get started, please see the :ref:`install` and the :ref:`quickstart`. optimizers/CONMIN optimizers/ALPSO optimizers/UNO + optimizers/Egor diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst new file mode 100644 index 000000000..2559dd3bd --- /dev/null +++ b/doc/optimizers/Egor.rst @@ -0,0 +1,50 @@ +.. _egor: + +Egor +==== + +Egor is the surrogate-based Efficient Global Optimization (EGO) optimizer from the open-source +`EGObox `_ library. + +Egor uses `bayesian optimization `_ techniques +well-suited to find the global optimum of an expansive-to-evaluate black-box function. +Basically, it uses a surrogate model to approximate the objective function and +an infill criterion (aka acquisition function) to guide the search for the optimum. + +The pyOptSparse wrapper is derivative-free and targets single-objective, bounded, +continuous design spaces. Constraint values are passed to Egor with the pyOptSparse +constraint convention transformed to :math:`c(x) \le 0`. + +Installation +------------ + +Egor is made available through the `egobox `_ Python package. + +.. prompt:: + + $ pip install egobox + +``egobox`` is also available via conda-forge:: + + $ conda install -c conda-forge egobox + + +Options +------- + +Please refer to the Egor help for a complete listing of options and their default values. + +.. prompt:: + + $ python + >>> import egobox as egx + >>> help(egx.Egor) + >>> help(egx.GpConfig) + +pyoptSparse expects pickable objects while native Egor structures as GpConfig are not pickable. +To workaround this constraint, the pyOptSparse Egor wrapper uses dictionaries which are accepted by Egor +to update the default field values of Egor structures. +Names and default values of the fields are provided in the descriptions below. + +.. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor + :filename: Egor_options.yaml diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml new file mode 100644 index 000000000..0b497ecb6 --- /dev/null +++ b/doc/optimizers/Egor_options.yaml @@ -0,0 +1,98 @@ +gp_config: + desc: | + GpConfig as a dict used by Egor for surrogate model configuration. + Main defaults are: + + - ``regr_spec``: 1 (Constant) + - ``corr_spec``: 2 (Squared Exponential) + - ``kpls_dim``: no PLS dimensionality reduction (otherwise int) + - ``n_clusters``: no clustering (otherwise int) +cstr_tol: + desc: | + Constraint tolerances list passed to Egor (size n_cstr plus n_fcstr) + Default is ``1e-4`` for all constraints. +n_start: + desc: Number of infill optimization runs (best run selected) +n_doe: + desc: Number of initial DOE samples (0 lets Egor choose automatically) +doe: + desc: | + Initial DOE array, either x-only or concatenated x and y + to be passed as list of lists of floats. If not provided, Egor will generate a DOE automatically. +infill_strategy: + desc: | + Infill criterion: + + - 1 = ``EI``, + - 2 = ``WB2``, + - 3 = ``WB2S``, + - 4 = ``LOG_EI`` (default) +cstr_infill: + desc: Enable constrained infill criterion (aka CEI) +cstr_strategy: + desc: | + Constraint strategy enum for surrogate constraint handling: + + - 1 = ``MeanConstraint`` (default) + - 2 = ``UpperConfidenceBound`` +qei_config: + desc: | + QEiConfig for batch (qEI) point selection passed as a dict with main keys being: + + - ``batch``: size of batch (int) + - ``strategy``: qEI strategy enum +infill_optimizer: + desc: | + Internal infill optimizer: + + - 1 = ``COBYLA`` (default) + - 2 = ``SLSQP`` +trego: + desc: | + Enable TREGO (aka Trust Region EGO) algorithm configured with main parameters: + + - ``n_gl_steps``: (nb of global search steps default 1, nb of local search steps default 4) + - ``beta``: trust region factor (default 0.9) + +coego_n_coop: + desc: Number of cooperative groups for CoEGO algorithm +target: + desc: Known objective target used as stopping criterion +outdir: + desc: Output directory for Egor output files (configuration, does, history and warm start search) +warm_start: + desc: Load initial DOE from outdir when enabled +hot_start: + desc: | + Egor checkpoint restart parameter to be used in case of fallible environment + to continue with the same Egor parameterization till max iterations or timeout is reached. +failsafe_strategy: + desc: | + Failure handling enum + + - 1 = ``REJECTION`` (default), + - 2 = ``IMPUTATION``, + - 3 = ``PROBA OF VIABILITY`` +seed: + desc: Seed for random number generator (default -1 for random seed) +verbose: + desc: | + Verbosity level for Egor logging + + - 0 = ``error`` (default) + - 1 = ``warning`` + - 2 = ``info`` + - 3 = ``debug`` +max_iters: + desc: Egor iteration budget +run_info: + desc: Optional RunInfo used to pass additional information to Egor (e.g., for logging) +timeout: + desc: Optional timeout in seconds used as sttopping criterion for Egor minimize +fcstrs: + desc: | + Optional list of native Egobox function constraints passed directly as fcstrs + (instead of pyOptSparse constraints which are metamodelized) +fcstr_specs: + desc: | + Optional list of egobox.CstrSpec for function constraints passed as fcstrs diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 134a5a4fc..ff3428054 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -21,6 +21,7 @@ from .pyNSGA2.pyNSGA2 import NSGA2 from .pyALPSO.pyALPSO import ALPSO from .pyParOpt.ParOpt import ParOpt +from .pyEgor.pyEgor import Egor __all__ = [ "History", @@ -43,6 +44,7 @@ "NSGA2", "ALPSO", "ParOpt", + "Egor", "testing", "list_optimizers", ] diff --git a/pyoptsparse/pyEgor/__init__.py b/pyoptsparse/pyEgor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py new file mode 100644 index 000000000..fbe23a1d9 --- /dev/null +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -0,0 +1,248 @@ +""" +pyEgor - A pyOptSparse interface to Egor from egobox. +""" + +# Standard Python modules +import datetime +import time + +# External modules +import numpy as np + +# Local modules +from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform +from ..pyOpt_utils import import_module + +# import the Python module +egobox = import_module("egobox") + + +class Egor(Optimizer): + """ + Egor Optimizer Class - wrapper for the Egor global optimization algorithm from egobox. + """ + + def __init__(self, raiseError=True, options=None): + if options is None: + options = {} + name = "Egor" + category = "Global Optimizer" + defOpts = self._getDefaultOptions() + informs = self._getInforms() + super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) + + if isinstance(egobox, Exception) and raiseError: + raise egobox + + @staticmethod + def _getInforms(): + informs = { + 1: "Reached maximum number of iterations", + 2: "Reached target cost function value", + 3: "Algorithm manually interrupted with SIGINT (Ctrl+C), SIGTERM or SIGHUP", + 4: "Algorithm peek at the same point twice. We consider it is converged.", + 5: "Timeout reached", + 6: "Solver unexpected exit. See logs for details.", + } + return informs + + @staticmethod + def _getDefaultOptions(): + defOpts = { + "gp_config": [dict, dict()], # GpConfig as a dict used by Egor for surrogate model configuration + "cstr_tol": [list, []], + "n_start": [int, 20], + "n_doe": [int, 0], + "doe": [list, [[]]], + "infill_strategy": [int, 4], # default to LOG_EI + "cstr_infill": [bool, False], + "cstr_strategy": [int, 1], # default to MC + "qei_config": [dict, dict()], + "infill_optimizer": [int, 1], # default to COBYLA + "trego": [dict, dict()], + "coego_n_coop": [int, 0], + "target": [float, -1e12], + "outdir": [str, ""], + "warm_start": [bool, False], + "hot_start": [bool, False], + "failsafe_strategy": [int, 1], # default to REJECTION + "seed": [int, -1], + "verbose": [int, 0], # level of verbosity, 0 = error, 1 = warn, 2 = info, 3 = debug + "max_iters": [int, 20], + "run_info": [dict, dict()], + "timeout": [float, -1.0], + "fcstrs": [list, []], + "fcstr_specs": [list, []], + } + return defOpts + + def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): + """ + Solve optimization problem using Egor from egobox. + + Parameters + ---------- + optProb : Optimization or Solution class instance + This is the complete description of the optimization problem + to be solved by the optimizer + + storeHistory : str + File name of the history file into which the history of + this optimization will be stored + + hotStart : str + File name of the history file to "replay" for the + optimization. The optimization problem used to generate + the history file specified in 'hotStart' must be + **IDENTICAL** to the currently supplied 'optProb'. By + identical we mean, **EVERY SINGLE PARAMETER MUST BE + IDENTICAL**. As soon as he requested evaluation point + from NSGA2 does not match the history, function and + gradient evaluations revert back to normal evaluations. + + Notes + ----- + The kwargs are present for compatibility with other optimizers. + Any sensitivity settings are ignored because Egor is derivative-free. + """ + self.startTime = time.time() + self.callCounter = 0 + + # Save the optimization problem and finalize constraint Jacobian + self.optProb = optProb + self.optProb.finalize() + + # Egor currently supports a single objective in this wrapper. + if len(self.optProb.objectives) != 1: + raise ValueError("Egor wrapper currently supports single-objective problems only.") + + # Set history/hotstart/coldstart + self._setHistory(storeHistory, hotStart) + self._setInitialCacheValues() + + if len(optProb.constraints) == 0: + self.unconstrained = True + + blx, bux, xs = self._assembleContinuousVariables() + xs = np.maximum(xs, blx) + xs = np.minimum(xs, bux) + n = len(xs) + + if np.any(~np.isfinite(blx)) or np.any(~np.isfinite(bux)): + raise ValueError("Egor requires finite lower and upper bounds for all design variables.") + + # Determine the number of constraints and set up constraint information + if self.unconstrained: + n_cstr = 0 + else: + indices, blc, buc, fact = self.optProb.getOrdering(["ne", "le", "ni", "li"], oneSided=True, noEquality=True) + n_cstr = len(indices) + self.optProb.jacIndices = indices + self.optProb.fact = fact + self.optProb.offset = buc + + if self.optProb.comm.rank == 0: + opt = self.getOption + + # Build x specifications from pyOptSparse bounds. + xspecs = [egobox.XSpec(egobox.XType.FLOAT, [float(blx[i]), float(bux[i])]) for i in range(n)] + + gp_config = opt("gp_config") + infill_strategy = opt("infill_strategy") + cstr_strategy = opt("cstr_strategy") + qei_config = opt("qei_config") + infill_optimizer = opt("infill_optimizer") + failsafe_strategy = opt("failsafe_strategy") + + fcstrs_opt = opt("fcstrs") + fcstr_specs = opt("fcstr_specs") + + n_fcstrs = 0 if fcstrs_opt is None else len(fcstrs_opt) + if fcstr_specs is not None and len(fcstr_specs) not in (0, n_fcstrs): + raise ValueError( + "Option 'fcstr_specs' length must be zero or match the number of function constraints." + ) + + # Prepare the constructor kwargs for Egor. + ctor_kwargs = { + "gp_config": gp_config, + "n_cstr": n_cstr, + "cstr_tol": opt("cstr_tol") if len(opt("cstr_tol")) > 0 else None, + "n_start": opt("n_start"), + "n_doe": opt("n_doe"), + "doe": np.array(opt("doe")) if np.array(opt("doe")).size > 0 else None, + "infill_strategy": infill_strategy, + "cstr_infill": opt("cstr_infill"), + "cstr_strategy": cstr_strategy, + "qei_config": qei_config, + "infill_optimizer": infill_optimizer, + "trego": opt("trego") if opt("trego") else None, + "coego_n_coop": opt("coego_n_coop"), + "target": float(opt("target")), + "failsafe_strategy": failsafe_strategy, + } + solver = egobox.Egor(xspecs, **ctor_kwargs) + + # Adapt the objective and constraint function to the Egor interface. + def fun(x): + x_eval = np.atleast_2d(np.asarray(x, dtype=float)) + ncols = 1 + n_cstr + y = np.zeros((x_eval.shape[0], ncols), dtype=float) + for i in range(x_eval.shape[0]): + xi = np.clip(x_eval[i], blx, bux) + fobj, fcon, fail = self._masterFunc(xi, ["fobj", "fcon"]) + if fail: + y[i, :] = np.nan + continue + y[i, 0] = float(np.atleast_1d(fobj)[0]) + if n_cstr > 0: + y[i, 1:] = np.asarray(fcon, dtype=float) + return y + + fcstrs = [] if fcstrs_opt is None else list(fcstrs_opt) + + # Prepare the minimize kwargs for Egor minimize. + minimize_kwargs = { + "fcstrs": fcstrs, + "fcstr_specs": [] if fcstr_specs is None else fcstr_specs, + "max_iters": opt("max_iters"), + "run_info": opt("run_info"), + "outdir": opt("outdir") if opt("outdir") != "" else None, + "warm_start": opt("warm_start"), + "hot_start": True if opt("hot_start") else False, + "seed": opt("seed") if opt("seed") >= 0 else None, + "timeout": float(opt("timeout")) if opt("timeout") > 0 else None, + "verbose": opt("verbose"), + } + + t0 = time.time() + egor_result = solver.minimize(fun, **minimize_kwargs) + optTime = time.time() - t0 + + if self.storeHistory: + self.metadata["endTime"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.metadata["optTime"] = optTime + self.hist.writeData("metadata", self.metadata) + self.hist.close() + + # Broadcast a -1 to indicate optimization has finished + self.optProb.comm.bcast(-1, root=0) + + # Optimizer has no standardized exit code mapping in this wrapper. + sol_inform = SolutionInform.from_informs(self.informs, int(egor_result.status.exit)) + + result = egor_result.result + + xstar = np.asarray(result.x_opt, dtype=float).reshape(-1) + ystar = np.asarray(result.y_opt, dtype=float).reshape(-1) + fstar = float(ystar[0]) + + # Create the optimization solution + sol = self._createSolution(optTime, sol_inform, fstar, xstar) + else: + self._waitLoop() + sol = None + + sol = self._communicateSolution(sol) + return sol diff --git a/pyoptsparse/pyOpt_optimizer.py b/pyoptsparse/pyOpt_optimizer.py index 2a13d14ce..b3f723dcc 100644 --- a/pyoptsparse/pyOpt_optimizer.py +++ b/pyoptsparse/pyOpt_optimizer.py @@ -973,7 +973,7 @@ def getInform(self, infocode: int | None = None) -> str | dict[int, str]: # ============================================================================= # List of optimizers as an enum -Optimizers = Enum("Optimizers", "SNOPT IPOPT Uno SLSQP NLPQLP CONMIN NSGA2 PSQP ALPSO ParOpt") +Optimizers = Enum("Optimizers", "SNOPT IPOPT Uno SLSQP NLPQLP CONMIN NSGA2 PSQP ALPSO ParOpt Egor") """Special enum containing all possible optimizers""" @@ -1020,6 +1020,8 @@ def OPT(optName, *args, **kwargs) -> Optimizer: from .pyALPSO.pyALPSO import ALPSO as opt elif optName == "paropt" or optName == Optimizers.ParOpt: from .pyParOpt.ParOpt import ParOpt as opt + elif optName == "egor" or optName == Optimizers.Egor: + from .pyEgor.pyEgor import Egor as opt else: raise ValueError( ( diff --git a/pyoptsparse/testing/pyOpt_testing.py b/pyoptsparse/testing/pyOpt_testing.py index 6c1ea6c57..b381b04a5 100644 --- a/pyoptsparse/testing/pyOpt_testing.py +++ b/pyoptsparse/testing/pyOpt_testing.py @@ -55,6 +55,7 @@ def get_dict_distance(d, d2): "NLPQLP": {"iFile": ".out"}, "ParOpt": {"output_file": ".out", "tr_output_file": ".tr", "mma_output_file": ".mma"}, "ALPSO": {"filename": ".out"}, + "Egor": {}, "NSGA2": {}, "Uno": {"logger_stream": ".out"}, } diff --git a/pyproject.toml b/pyproject.toml index b3723001f..d9b332a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "sqlitedict>=1.6", "numpy>=1.25", "scipy>=1.11", - "mdolab-baseclasses>=1.3.1" + "mdolab-baseclasses>=1.3.1", ] dynamic = ["version"] # version is dynamically populated from meson project @@ -30,6 +30,7 @@ docs = [ testing = [ "testflo>=1.4.5", "parameterized", + "egobox>=0.37.6,<0.38.0", ] dev = [ "meson-python", diff --git a/tests/test_egor.py b/tests/test_egor.py new file mode 100644 index 000000000..6a9803c19 --- /dev/null +++ b/tests/test_egor.py @@ -0,0 +1,173 @@ +"""Test class for Egor specific tests""" + +# Standard Python modules +import json +import tempfile + +# External modules +import numpy as np + +# First party modules +from pyoptsparse import Optimization +from pyoptsparse.testing import OptTest + + +class TestEgor(OptTest): + def setup_xsinx_optProb(self): + """ + Setup the optimization problem for the xsinx function. + + The xsinx function is defined as: + f(x) = (x - 3.5) * sin((x - 3.5) / π) + + Domain: x ∈ [0, 25] + Solution opt pb: fStar ≈ -15.1 at x ≈ 18.935 + """ + + def objfunc(xdict): + x = xdict["x"] + funcs = {} + # xsinx function: (x - 3.5) * sin((x - 3.5) / π) + funcs["obj"] = (x - 3.5) * np.sin((x - 3.5) / np.pi) + fail = False + return funcs, fail + + optProb = Optimization("xsinx Function", objfunc) + optProb.addVar("x", lower=0.0, upper=25.0) + optProb.addObj("obj") + self.optName = "Egor" + self.optProb = optProb + + def test_egor(self): + self.setup_xsinx_optProb() + sol = self.optimize() + # Check Solution + self.assertLess(sol.fStar, -15.0) + + def test_egor_inform(self): + self.setup_xsinx_optProb() + # Test that the inform is "Maximum number of iterations reached" + sol = self.optimize(optOptions={"max_iters": 1}) + self.assert_inform_equal(sol, 1) + + # Test that the inform is "Target function value reached" + sol = self.optimize(optOptions={"target": -10.0}) + self.assert_inform_equal(sol, 2) + + # Test that the inform is "Time limit reached" + sol = self.optimize(optOptions={"timeout": 1e-6}) + self.assert_inform_equal(sol, 5) + + def test_egor_warm_start(self): + with tempfile.TemporaryDirectory() as outdir: + self.setup_xsinx_optProb() + # First run to generate a history file + sol1 = self.optimize(optOptions={"max_iters": 1, "outdir": outdir, "seed": 0}) + # Second run with warm start + sol2 = self.optimize(optOptions={"max_iters": 5, "outdir": outdir, "warm_start": True}) + # Check that the second run continued from the first run + self.assertGreater(sol1.fStar, sol2.fStar) + + def test_egor_config(self): + with tempfile.TemporaryDirectory() as outdir: + self.setup_xsinx_optProb() + # Test that the gp_config option is passed correctly + gp_config = {"corr_spec": 4, "kpls_dim": 1} + _ = self.optimize( + optOptions={ + "infill_strategy": 1, + "gp_config": gp_config, + "outdir": outdir, + "trego": {"n_gl_steps": (1, 3)}, + } + ) + # read egor_config.json from outdir and check that corr_spec is 4 + with open(f"{outdir}/egor_config.json", "r") as f: + egor_config = json.load(f) + self.assertEqual(egor_config["gp"]["correlation_spec"], "MATERN32") + self.assertEqual(egor_config["gp"]["kpls_dim"], 1) + self.assertEqual(egor_config["infill_criterion"]["type_infill"], "ExpectedImprovement") + self.assertEqual(egor_config["iteration_strategy"]["type_iteration_strategy"], "TregoStrategy") + self.assertEqual(egor_config["iteration_strategy"]["n_gl_steps"], [1, 3]) + + def test_egor_ackley(self): + """ + Test that Egor can optimize the Ackley function. + """ + + def objfunc(xdict): + x = xdict["xvars"] + funcs = {} + funcs["obj"] = ( + -20.0 * np.exp(-0.2 * np.sqrt(0.5 * (x[0] ** 2 + x[1] ** 2))) + - np.exp(0.5 * (np.cos(2.0 * np.pi * x[0]) + np.cos(2.0 * np.pi * x[1]))) + + np.e + + 20 + ) + fail = False + return funcs, fail + + optProb = Optimization("Ackley Function", objfunc) + optProb.addVarGroup("xvars", 2, lower=[-32.768, -32.768], upper=[32.768, 32.768]) + optProb.addObj("obj") + self.optName = "Egor" + self.optProb = optProb + sol = self.optimize( + optOptions={ + "max_iters": 100, + "verbose": 2, # level of verbosity, 0 = error, 1 = warn, 2 = info, 3 = debug + "n_doe": 15, + "gp_config": { + "corr_spec": 8 + }, # corr spec: 1 = absolute exponential, 2 = squared exponential, 4 = matern 3/2, 8 = matern 5/2 + "seed": 0, + "trego": {"n_gl_steps": (1, 4)}, + } + ) + # Check Solution + self.fStar = [0.0] + self.xStar = [ + {"xvars": (0.0, 0.0)}, + ] + self.assert_solution_allclose(sol, tol=1e-2) + + def test_egor_g24(self): + """ + Test Egor on the G24 problem. + + The G24 problem is defined as: + minimize f(x) = -x1 - x2 + subject to: + c1(x) = -2*x1^4 + 8*x1^3 - 8*x1^2 + x2 - 2 <= 0 + c2(x) = -4*x1^4 + 32*x1^3 - 88*x1^2 + 96*x1 + x2 - 36 <= 0 + with x1 in [0, 3] and x2 in [0, 4] + + Global optimum: x_opt = (2.3295, 3.1785), f_opt = -5.5080 + """ + + def objfunc(xdict): + x = xdict["xvars"] + funcs = {} + funcs["obj"] = -x[0] - x[1] + funcs["con"] = [ + -2.0 * x[0] ** 4 + 8.0 * x[0] ** 3 - 8.0 * x[0] ** 2 + x[1] - 2.0, + -4.0 * x[0] ** 4 + 32.0 * x[0] ** 3 - 88.0 * x[0] ** 2 + 96.0 * x[0] + x[1] - 36.0, + ] + fail = False + return funcs, fail + + optProb = Optimization("G24 Function", objfunc) + optProb.addVarGroup("xvars", 2, lower=[0.0, 0.0], upper=[3.0, 4.0]) + optProb.addObj("obj") + optProb.addConGroup("con", 2, upper=0.0) + self.optName = "Egor" + self.optProb = optProb + sol = self.optimize( + optOptions={"max_iters": 30, "n_doe": 5, "target": -5.50, "cstr_tol": [1e-3, 1e-3], "verbose": 2} + ) + # Check Solution + self.fStar = [-5.5080] + self.xStar = [ + {"xvars": (2.3295, 3.1785)}, + ] + self.assert_solution_allclose(sol, tol=1e-2) diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 4da28a180..9ac45f64e 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -47,8 +47,16 @@ class TestHS15(OptTest): "IPOPT": 1e-4, "CONMIN": 1e-10, "PSQP": 5e-12, + "Uno": 1e-4, + "Egor": 5e-2, + } + optOptions = { + "Egor": { + "max_iters": 50, + "n_doe": 30, + "seed": 42, + } } - optOptions = {} def objfunc(self, xdict): self.nf += 1 @@ -116,11 +124,11 @@ def test_snopt(self): # sol_xvars = [sol.variables["xvars"][i].value for i in range(2)] # assert_allclose(sol_xvars, dv["xvars"], atol=tol, rtol=tol) - @parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP"]) + @parameterized.expand(["SLSQP", "PSQP", "CONMIN", "NLPQLP", "Egor"]) def test_optimization(self, optName): self.optName = optName self.setup_optProb() - optOptions = self.optOptions.pop(optName, None) + optOptions = self.optOptions.get(optName, None) sol = self.optimize(optOptions=optOptions) # Check Solution self.assert_solution_allclose(sol, self.tol[optName]) diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 1ca35b010..cb6b61b06 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -33,12 +33,14 @@ class TestHS71(OptTest): "CONMIN": 1e-3, "PSQP": 1e-6, "Uno": 1e-4, + "Egor": 1e-2, } optOptions = { "CONMIN": { "DELFUN": 1e-10, "DABFUN": 1e-10, - } + }, + "Egor": {"max_iters": 100, "seed": 42, "trego": {"n_gl_steps": (1, 4)}}, } def objfunc(self, xdict): @@ -257,7 +259,7 @@ def test_psqp_informs(self): sol = self.optimize(optOptions={"MIT": 1}) self.assert_inform_equal(sol, 11) - @parameterized.expand(["SNOPT", "IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno"]) + @parameterized.expand(["SNOPT", "IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno", "Egor"]) def test_optimization(self, optName): self.optName = optName self.setup_optProb() diff --git a/tests/test_sphere.py b/tests/test_sphere.py index 2dc770142..dbe162435 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -41,7 +41,7 @@ class TestSphere(OptTest): xStar = {"xvars": np.zeros(N)} # Tolerances - tol = {k: 5e-2 if k in ["CONMIN", "ALPSO", "NSGA2"] else 1e-6 for k in ALL_OPTIMIZERS} + tol = {k: 5e-2 if k in ["CONMIN", "ALPSO", "NSGA2", "Egor"] else 1e-6 for k in ALL_OPTIMIZERS} optOptions = { "ALPSO": { # sphere @@ -61,6 +61,7 @@ class TestSphere(OptTest): "Major iterations limit": 10, }, "Uno": {"max_iterations": 100, "preset": "filtersqp"}, + "Egor": {"max_iters": 100, "seed": 123, "trego": {"n_gl_steps": (1, 4)}}, } def objfunc(self, xdict):