From 5ae2b592527a1d8d739571385c36806c92ec8b25 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 8 Jun 2026 15:28:01 +0200 Subject: [PATCH 01/33] Initial integration --- doc/index.rst | 1 + doc/optimizers/Egor.rst | 23 +++ doc/optimizers/Egor_options.yaml | 48 ++++++ pyoptsparse/__init__.py | 2 + pyoptsparse/pyEgor/__init__.py | 0 pyoptsparse/pyEgor/pyEgor.py | 249 +++++++++++++++++++++++++++++++ pyoptsparse/pyOpt_optimizer.py | 4 +- 7 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 doc/optimizers/Egor.rst create mode 100644 doc/optimizers/Egor_options.yaml create mode 100644 pyoptsparse/pyEgor/__init__.py create mode 100644 pyoptsparse/pyEgor/pyEgor.py diff --git a/doc/index.rst b/doc/index.rst index c9d1b0fd7..d1b5d9887 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -69,4 +69,5 @@ To get started, please see the :ref:`install` and the :ref:`quickstart`. optimizers/ParOpt optimizers/CONMIN optimizers/ALPSO + optimizers/Egor optimizers/UNO diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst new file mode 100644 index 000000000..46d3547e7 --- /dev/null +++ b/doc/optimizers/Egor.rst @@ -0,0 +1,23 @@ +.. _egor: + +Egor +==== + +Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided by the +`egobox `_ package. + +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`. + +Options +------- +.. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor + :filename: Egor_options.yaml + +API +--- +.. currentmodule:: pyoptsparse.pyEgor.pyEgor + +.. autoclass:: Egor + :members: __call__ diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml new file mode 100644 index 000000000..0c2258085 --- /dev/null +++ b/doc/optimizers/Egor_options.yaml @@ -0,0 +1,48 @@ +gp_config: + desc: GpConfig instance used by Egor for surrogate model configuration +cstr_tol: + desc: Constraint tolerances list passed to Egor (size n_cstr plus n_fcstr) +fcstrs: + desc: Optional list of native Egobox function constraints passed to minimize +fcstr_specs: + desc: Optional list of egobox.CstrSpec for function constraints passed as fcstrs +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 +infill_strategy: + desc: Infill criterion enum (EI, WB2, WB2S, LOG_EI) +cstr_infill: + desc: Enable constrained infill criterion +cstr_strategy: + desc: Constraint strategy enum for surrogate constraint handling +qei_config: + desc: QEiConfig for batch (qEI) point selection +infill_optimizer: + desc: Internal infill optimizer enum (COBYLA or SLSQP) +trego: + desc: TREGO configuration (TregoConfig, True for defaults, or None) +coego_n_coop: + desc: Number of cooperative groups for CoEGO mode +target: + desc: Known objective target used as stopping criterion +outdir: + desc: Output directory for Egor history and warm start search +warm_start: + desc: Load initial DOE from outdir when enabled +hot_start: + desc: Egor checkpoint restart/extension parameter +failsafe_strategy: + desc: Failure handling enum (REJECTION, IMPUTATION, VIABILITY) +seed: + desc: Constructor seed (deprecated in egobox in favor of minimize seed) +verbose: + desc: Constructor verbosity (deprecated in egobox in favor of minimize verbose) +max_iters: + desc: Egor minimize iteration budget +run_info: + desc: Optional RunInfo object passed to Egor minimize +timeout: + desc: Optional minimize timeout in seconds 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..1c65178b1 --- /dev/null +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -0,0 +1,249 @@ +""" +pyEgor - A pyOptSparse interface to Egor from egobox. +""" + +# Standard Python modules +import datetime +import inspect +import time + +# External modules +import numpy as np + +# Local modules +from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import import_module + +# import the Python module +egobox = import_module("egobox") + + +class Egor(Optimizer): + """ + Egor Optimizer Class - Inherited from Optimizer Abstract Class + """ + + 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 = {} + return informs + + @staticmethod + def _getDefaultOptions(): + defOpts = { + "gp_config": [object, None], + "cstr_tol": [object, None], + "fcstrs": [object, None], + "fcstr_specs": [object, None], + "n_start": [int, 20], + "n_doe": [int, 0], + "doe": [object, None], + "infill_strategy": [object, None], + "cstr_infill": [bool, False], + "cstr_strategy": [object, None], + "qei_config": [object, None], + "infill_optimizer": [object, None], + "trego": [object, None], + "coego_n_coop": [int, 0], + "target": [float, -np.finfo(float).max], + "outdir": [object, None], + "warm_start": [bool, False], + "hot_start": [object, None], + "failsafe_strategy": [object, None], + "seed": [object, None], + "verbose": [object, None], + "max_iters": [int, 20], + "run_info": [object, None], + "timeout": [object, None], + } + return defOpts + + def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): + """ + Solve optimization problem using Egor from egobox. + + Notes + ----- + The kwargs are present for compatibility with other optimizers. + Any sensitivity settings are ignored because Egor is derivative-free. + """ + self.startTime = time.time() + + # 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.") + + if self.unconstrained: + m = 0 + else: + indices, blc, buc, fact = self.optProb.getOrdering( + ["ne", "le", "ni", "li"], oneSided=True, noEquality=True + ) + m = 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") + if gp_config is None: + gp_config = egobox.GpConfig() + + infill_strategy = opt("infill_strategy") + if infill_strategy is None: + infill_strategy = egobox.InfillStrategy.LOG_EI + + cstr_strategy = opt("cstr_strategy") + if cstr_strategy is None: + cstr_strategy = egobox.ConstraintStrategy.MC + + qei_config = opt("qei_config") + if qei_config is None: + qei_config = egobox.QEiConfig() + + infill_optimizer = opt("infill_optimizer") + if infill_optimizer is None: + infill_optimizer = egobox.InfillOptimizer.COBYLA + + failsafe_strategy = opt("failsafe_strategy") + if failsafe_strategy is None: + failsafe_strategy = egobox.FailsafeStrategy.REJECTION + + fcstrs_opt = opt("fcstrs") + n_cstr = m + 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.") + + ctor_supported = set(inspect.signature(egobox.Egor).parameters.keys()) + supports_ctor_verbose = "verbose" in ctor_supported + + if opt("verbose") is not None and not supports_ctor_verbose: + raise ValueError("Installed egobox version does not support constructor option 'verbose'.") + + ctor_kwargs = { + "gp_config": gp_config, + "n_cstr": n_cstr, + "cstr_tol": opt("cstr_tol"), + "n_start": opt("n_start"), + "n_doe": opt("n_doe"), + "doe": opt("doe"), + "infill_strategy": infill_strategy, + "cstr_infill": opt("cstr_infill"), + "cstr_strategy": cstr_strategy, + "qei_config": qei_config, + "infill_optimizer": infill_optimizer, + "trego": opt("trego"), + "coego_n_coop": opt("coego_n_coop"), + "target": opt("target"), + "outdir": opt("outdir"), + "warm_start": opt("warm_start"), + "hot_start": opt("hot_start"), + "failsafe_strategy": failsafe_strategy, + "seed": opt("seed"), + "verbose": opt("verbose"), + } + ctor_kwargs = {k: v for k, v in ctor_kwargs.items() if k in ctor_supported} + solver = egobox.Egor(xspecs, **ctor_kwargs) + + def fun(x): + x_eval = np.atleast_2d(np.asarray(x, dtype=float)) + ncols = 1 + m + 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 m > 0: + y[i, 1:] = np.asarray(fcon, dtype=float) + return y + + fcstrs = [] if fcstrs_opt is None else list(fcstrs_opt) + + 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"), + "warm_start": opt("warm_start"), + "hot_start": opt("hot_start"), + "seed": opt("seed"), + "timeout": opt("timeout"), + "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 = None + + 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( ( From c131d431c21331a76995d60ffa544cae06a86015 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 8 Jun 2026 16:25:40 +0200 Subject: [PATCH 02/33] Add some tests --- pyoptsparse/pyEgor/pyEgor.py | 8 ++------ pyoptsparse/testing/pyOpt_testing.py | 1 + tests/test_hs015.py | 14 +++++++++++--- tests/test_hs071.py | 9 +++++++-- tests/test_rosenbrock.py | 4 +++- tests/test_sphere.py | 2 +- uno_log_SILENT.txt | 0 7 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 uno_log_SILENT.txt diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 1c65178b1..f94eac4ef 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -60,7 +60,7 @@ def _getDefaultOptions(): "target": [float, -np.finfo(float).max], "outdir": [object, None], "warm_start": [bool, False], - "hot_start": [object, None], + "hot_start": [bool, False], "failsafe_strategy": [object, None], "seed": [object, None], "verbose": [object, None], @@ -178,11 +178,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "coego_n_coop": opt("coego_n_coop"), "target": opt("target"), "outdir": opt("outdir"), - "warm_start": opt("warm_start"), - "hot_start": opt("hot_start"), "failsafe_strategy": failsafe_strategy, - "seed": opt("seed"), - "verbose": opt("verbose"), } ctor_kwargs = {k: v for k, v in ctor_kwargs.items() if k in ctor_supported} solver = egobox.Egor(xspecs, **ctor_kwargs) @@ -211,7 +207,7 @@ def fun(x): "run_info": opt("run_info"), "outdir": opt("outdir"), "warm_start": opt("warm_start"), - "hot_start": opt("hot_start"), + "hot_start": True if opt("hot_start") else False, "seed": opt("seed"), "timeout": opt("timeout"), "verbose": opt("verbose"), 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/tests/test_hs015.py b/tests/test_hs015.py index 4da28a180..00c1240ec 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": 12, + "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..9cea3be14 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -33,12 +33,17 @@ 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 + }, } def objfunc(self, xdict): @@ -257,7 +262,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_rosenbrock.py b/tests/test_rosenbrock.py index 40619cf99..0e5bfa0a5 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -48,10 +48,12 @@ class TestRosenbrock(OptTest): "CONMIN": 1e-9, "PSQP": 1e-8, "Uno": 1e-4, + "Egor": 5e1, } optOptions = { "SLSQP": {"ACC": 1e-10}, "NLPQLP": {"accuracy": 1e-10}, + "Egor": {"max_iters": 100, "trego": True, "n_doe": 60, "seed": 42}, } def objfunc(self, xdict): @@ -144,7 +146,7 @@ def test_snopt_hotstart_starting_from_grad(self): # The first is from a call we deleted and the second is the call after 'last' self.assertEqual(self.ng, 2) - @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno"]) + @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno", "Egor"]) def test_optimization(self, optName): self.optName = optName if optName == "IPOPT" and sys.platform == "win32": diff --git a/tests/test_sphere.py b/tests/test_sphere.py index 2dc770142..ca8f132d4 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -12,7 +12,7 @@ from pyoptsparse.pyOpt_optimizer import Optimizers from pyoptsparse.testing import OptTest -ALL_OPTIMIZERS = sorted({e.name for e in Optimizers} - {"ParOpt", "NSGA2"}) +ALL_OPTIMIZERS = sorted({e.name for e in Optimizers} - {"ParOpt", "NSGA2", "Egor"}) class TestSphere(OptTest): diff --git a/uno_log_SILENT.txt b/uno_log_SILENT.txt new file mode 100644 index 000000000..e69de29bb From 5daa2cb51f9d4b8265b735c1759b93a2c0f18100 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 9 Jun 2026 14:31:23 +0200 Subject: [PATCH 03/33] Adjust doc --- doc/optimizers/Egor.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index 46d3547e7..d1e31c77e 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -4,7 +4,11 @@ Egor ==== Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided by the -`egobox `_ package. +`egobox `_ package which is intalled with: + +.. prompt:: bash + + pip install egobox The pyOptSparse wrapper is derivative-free and targets single-objective, bounded, continuous design spaces. Constraint values are passed to Egor with the pyOptSparse From 2952cff4ba0550bef64c4268d5041de177467321 Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 14 Jun 2026 20:48:35 +0200 Subject: [PATCH 04/33] Test on ackley instead of rosenbrock --- doc/optimizers/Egor.rst | 5 +++++ tests/test_egor.py | 41 ++++++++++++++++++++++++++++++++++++++++ tests/test_rosenbrock.py | 4 +--- 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/test_egor.py diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index d1e31c77e..dcef51007 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -10,6 +10,11 @@ Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided pip install egobox +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`. diff --git a/tests/test_egor.py b/tests/test_egor.py new file mode 100644 index 000000000..2da197e1f --- /dev/null +++ b/tests/test_egor.py @@ -0,0 +1,41 @@ +"""Test class for Egor specific tests""" + +import numpy as np + +# First party modules +from pyoptsparse import OPT, Optimization +from pyoptsparse.testing import OptTest + + +class TestEgor(OptTest): + + 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}) + # Check Solution + print("Solution: ", sol.xStar, "f: ", sol.fStar) + self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) + self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) + self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=1e-2) diff --git a/tests/test_rosenbrock.py b/tests/test_rosenbrock.py index 0e5bfa0a5..40619cf99 100644 --- a/tests/test_rosenbrock.py +++ b/tests/test_rosenbrock.py @@ -48,12 +48,10 @@ class TestRosenbrock(OptTest): "CONMIN": 1e-9, "PSQP": 1e-8, "Uno": 1e-4, - "Egor": 5e1, } optOptions = { "SLSQP": {"ACC": 1e-10}, "NLPQLP": {"accuracy": 1e-10}, - "Egor": {"max_iters": 100, "trego": True, "n_doe": 60, "seed": 42}, } def objfunc(self, xdict): @@ -146,7 +144,7 @@ def test_snopt_hotstart_starting_from_grad(self): # The first is from a call we deleted and the second is the call after 'last' self.assertEqual(self.ng, 2) - @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno", "Egor"]) + @parameterized.expand(["IPOPT", "SLSQP", "PSQP", "CONMIN", "NLPQLP", "Uno"]) def test_optimization(self, optName): self.optName = optName if optName == "IPOPT" and sys.platform == "win32": From 356e1cacdae4e7ea0fcff5cc29b93f0c696af238 Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 14 Jun 2026 23:18:36 +0200 Subject: [PATCH 05/33] Improve documentation and exit status handling --- doc/optimizers/Egor.rst | 15 +++++++++ doc/optimizers/Egor_options.yaml | 18 +++++------ pyoptsparse/pyEgor/pyEgor.py | 53 ++++++++++++++++++-------------- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index dcef51007..8cac88bcc 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -21,6 +21,21 @@ constraint convention transformed to :math:`c(x) \le 0`. Options ------- + +Please refer to the Egor help for a complete listing of options and their default values. + +.. prompt:: bash + + 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 index 0c2258085..9e340a522 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -1,11 +1,7 @@ gp_config: - desc: GpConfig instance used by Egor for surrogate model configuration + desc: GpConfig as a dict used by Egor for surrogate model configuration cstr_tol: desc: Constraint tolerances list passed to Egor (size n_cstr plus n_fcstr) -fcstrs: - desc: Optional list of native Egobox function constraints passed to minimize -fcstr_specs: - desc: Optional list of egobox.CstrSpec for function constraints passed as fcstrs n_start: desc: Number of infill optimization runs (best run selected) n_doe: @@ -23,7 +19,7 @@ qei_config: infill_optimizer: desc: Internal infill optimizer enum (COBYLA or SLSQP) trego: - desc: TREGO configuration (TregoConfig, True for defaults, or None) + desc: Enable treg coego_n_coop: desc: Number of cooperative groups for CoEGO mode target: @@ -37,12 +33,16 @@ hot_start: failsafe_strategy: desc: Failure handling enum (REJECTION, IMPUTATION, VIABILITY) seed: - desc: Constructor seed (deprecated in egobox in favor of minimize seed) + desc: Seed for random number generator (default -1 for random seed) verbose: - desc: Constructor verbosity (deprecated in egobox in favor of minimize verbose) + desc: Constructor verbosity max_iters: desc: Egor minimize iteration budget run_info: - desc: Optional RunInfo object passed to Egor minimize + desc: Optional RunInfo used to pass additional information to Egor (e.g., for logging) timeout: desc: Optional minimize timeout in seconds +fcstrs: + desc: Optional list of native Egobox function constraints passed to minimize +fcstr_specs: + desc: Optional list of egobox.CstrSpec for function constraints passed as fcstrs diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index f94eac4ef..8797d4ac9 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -11,6 +11,7 @@ import numpy as np # Local modules +from ..pyOpt_solution import SolutionInform from ..pyOpt_optimizer import Optimizer from ..pyOpt_utils import import_module @@ -37,36 +38,43 @@ def __init__(self, raiseError=True, options=None): @staticmethod def _getInforms(): - informs = {} + 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": [object, None], - "cstr_tol": [object, None], - "fcstrs": [object, None], - "fcstr_specs": [object, None], + "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": [object, None], - "infill_strategy": [object, None], + "doe": [list, [[]]], + "infill_strategy": [int, 4], # default to LOG_EI "cstr_infill": [bool, False], - "cstr_strategy": [object, None], - "qei_config": [object, None], - "infill_optimizer": [object, None], - "trego": [object, None], + "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, -np.finfo(float).max], - "outdir": [object, None], + "target": [float, -1e12], + "outdir": [str, ""], "warm_start": [bool, False], "hot_start": [bool, False], - "failsafe_strategy": [object, None], - "seed": [object, None], - "verbose": [object, None], + "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": [object, None], - "timeout": [object, None], + "run_info": [dict, dict()], + "timeout": [int, -1], + "fcstrs": [list, []], + "fcstr_specs": [list, []], } return defOpts @@ -165,10 +173,10 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): ctor_kwargs = { "gp_config": gp_config, "n_cstr": n_cstr, - "cstr_tol": opt("cstr_tol"), + "cstr_tol": opt("cstr_tol") if len(opt("cstr_tol")) > 0 else None, "n_start": opt("n_start"), "n_doe": opt("n_doe"), - "doe": opt("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, @@ -177,7 +185,6 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "trego": opt("trego"), "coego_n_coop": opt("coego_n_coop"), "target": opt("target"), - "outdir": opt("outdir"), "failsafe_strategy": failsafe_strategy, } ctor_kwargs = {k: v for k, v in ctor_kwargs.items() if k in ctor_supported} @@ -209,7 +216,7 @@ def fun(x): "warm_start": opt("warm_start"), "hot_start": True if opt("hot_start") else False, "seed": opt("seed"), - "timeout": opt("timeout"), + "timeout": opt("timeout") if opt("timeout") > 0 else None, "verbose": opt("verbose"), } @@ -227,7 +234,7 @@ def fun(x): self.optProb.comm.bcast(-1, root=0) # Optimizer has no standardized exit code mapping in this wrapper. - sol_inform = None + sol_inform = SolutionInform.from_informs(self.informs, int(egor_result.status.exit)) result = egor_result.result From 11f33210bc820b67012833e48d67be599f3681a1 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 10:09:57 +0200 Subject: [PATCH 06/33] More tests --- doc/optimizers/Egor_options.yaml | 2 +- pyoptsparse/pyEgor/pyEgor.py | 12 ++++---- tests/test_egor.py | 50 +++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml index 9e340a522..f7b8863b1 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -19,7 +19,7 @@ qei_config: infill_optimizer: desc: Internal infill optimizer enum (COBYLA or SLSQP) trego: - desc: Enable treg + desc: Enable TREGO (aka Trust Region EGO) algorithm coego_n_coop: desc: Number of cooperative groups for CoEGO mode target: diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 8797d4ac9..31c1338c6 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -21,7 +21,7 @@ class Egor(Optimizer): """ - Egor Optimizer Class - Inherited from Optimizer Abstract Class + Egor Optimizer Class - wrapper for the Egor global optimization algorithm from egobox. """ def __init__(self, raiseError=True, options=None): @@ -72,7 +72,7 @@ def _getDefaultOptions(): "verbose": [int, 0], # level of verbosity, 0 = error, 1 = warn, 2 = info, 3 = debug "max_iters": [int, 20], "run_info": [dict, dict()], - "timeout": [int, -1], + "timeout": [float, -1.], "fcstrs": [list, []], "fcstr_specs": [list, []], } @@ -184,7 +184,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "infill_optimizer": infill_optimizer, "trego": opt("trego"), "coego_n_coop": opt("coego_n_coop"), - "target": opt("target"), + "target": float(opt("target")), "failsafe_strategy": failsafe_strategy, } ctor_kwargs = {k: v for k, v in ctor_kwargs.items() if k in ctor_supported} @@ -212,11 +212,11 @@ def fun(x): "fcstr_specs": [] if fcstr_specs is None else fcstr_specs, "max_iters": opt("max_iters"), "run_info": opt("run_info"), - "outdir": opt("outdir"), + "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"), - "timeout": opt("timeout") if opt("timeout") > 0 else None, + "seed": opt("seed") if opt("seed") >= 0 else None, + "timeout": float(opt("timeout")) if opt("timeout") > 0 else None, "verbose": opt("verbose"), } diff --git a/tests/test_egor.py b/tests/test_egor.py index 2da197e1f..82a6efc37 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -6,9 +6,56 @@ from pyoptsparse import OPT, Optimization from pyoptsparse.testing import OptTest +import egobox as egx + 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 + print("xsinx Solution: ", sol.xStar, "f: ", sol.fStar) + self.assertLess(sol.fStar, -15.1) # Should find a negative minimum + + 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, int(egx.ExitStatus.MAX_ITERS_REACHED)) + + # Test that the inform is "Target function value reached" + sol = self.optimize(optOptions={"target": -10.}) + self.assert_inform_equal(sol, int(egx.ExitStatus.TARGET_COST_REACHED)) + + # Test that the inform is "Time limit reached" + sol = self.optimize(optOptions={"timeout": 1e-6}) + self.assert_inform_equal(sol, int(egx.ExitStatus.TIMEOUT)) + def test_egor_ackley(self): """ Test that Egor can optimize the Ackley function. @@ -35,7 +82,8 @@ def objfunc(xdict): "gp_config":{"corr_spec": 8}, # corr spec: 1 = absolute exponential, 2 = squared exponential, 4 = matern 3/2, 8 = matern 5/2 "seed": 0}) # Check Solution - print("Solution: ", sol.xStar, "f: ", sol.fStar) + print("Ackley Solution: ", sol.xStar, "f: ", sol.fStar) self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=1e-2) + From d55deb0c267e55ddce5d0dea7382c0861f484089 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 10:45:40 +0200 Subject: [PATCH 07/33] Test warm_start --- doc/optimizers/Egor_options.yaml | 4 +++- tests/test_egor.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml index f7b8863b1..fe9abd796 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -29,7 +29,9 @@ outdir: warm_start: desc: Load initial DOE from outdir when enabled hot_start: - desc: Egor checkpoint restart/extension parameter + 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 (REJECTION, IMPUTATION, VIABILITY) seed: diff --git a/tests/test_egor.py b/tests/test_egor.py index 82a6efc37..2aa13c954 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -1,6 +1,7 @@ """Test class for Egor specific tests""" import numpy as np +import tempfile # First party modules from pyoptsparse import OPT, Optimization @@ -54,7 +55,19 @@ def test_egor_inform(self): # Test that the inform is "Time limit reached" sol = self.optimize(optOptions={"timeout": 1e-6}) - self.assert_inform_equal(sol, int(egx.ExitStatus.TIMEOUT)) + self.assert_inform_equal(sol, int(egx.ExitStatus.TIMEOUT)) + + 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}) + print("First run: ", sol1.xStar, "f: ", sol1.fStar) + # Second run with warm start + sol2 = self.optimize(optOptions={"max_iters": 5, "outdir": outdir, "warm_start": True}) + print("Second run (warm start): ", sol2.xStar, "f: ", sol2.fStar) + # Check that the second run continued from the first run + self.assertGreater(sol1.fStar, sol2.fStar) def test_egor_ackley(self): """ From 8dd6b1b91405734bc0c8e08b1a36d9d2bbe76eb0 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 11:09:01 +0200 Subject: [PATCH 08/33] Test Egor configuration --- pyoptsparse/pyEgor/pyEgor.py | 2 +- tests/test_egor.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 31c1338c6..8943bfef0 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -182,7 +182,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "cstr_strategy": cstr_strategy, "qei_config": qei_config, "infill_optimizer": infill_optimizer, - "trego": opt("trego"), + "trego": opt("trego") if opt("trego") else None, "coego_n_coop": opt("coego_n_coop"), "target": float(opt("target")), "failsafe_strategy": failsafe_strategy, diff --git a/tests/test_egor.py b/tests/test_egor.py index 2aa13c954..fbb6ebaf7 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -69,6 +69,23 @@ def test_egor_warm_start(self): # 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} # Matern 3/2 + sol = 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 + import json + with open(f"{outdir}/egor_config.json", "r") as f: + egor_config = json.load(f) + print("Egor config: ", egor_config) + self.assertEqual(egor_config["gp"]["correlation_spec"], "MATERN32") + 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. From a0a1c2023787ea5c5247de1085d97a4407971ad8 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 11:13:53 +0200 Subject: [PATCH 09/33] Format --- doc/optimizers/Egor.rst | 8 ++--- doc/optimizers/Egor_options.yaml | 4 +-- pyoptsparse/pyEgor/pyEgor.py | 21 +++++------- tests/test_egor.py | 56 ++++++++++++++++++++------------ tests/test_hs071.py | 5 +-- 5 files changed, 51 insertions(+), 43 deletions(-) diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index 8cac88bcc..2cfd35d9d 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -10,9 +10,9 @@ Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided pip install egobox -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 +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, @@ -31,7 +31,7 @@ Please refer to the Egor help for a complete listing of options and their defaul >>> help(egx.Egor) >>> help(egx.GpConfig) -pyoptSparse expects pickable objects while native Egor structures as GpConfig are not pickable. +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. diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml index fe9abd796..8fe75cc08 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -29,8 +29,8 @@ outdir: 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 + 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 (REJECTION, IMPUTATION, VIABILITY) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 8943bfef0..801bcf1ec 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -60,19 +60,19 @@ def _getDefaultOptions(): "cstr_infill": [bool, False], "cstr_strategy": [int, 1], # default to MC "qei_config": [dict, dict()], - "infill_optimizer": [int, 1], # default to COBYLA + "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 + "failsafe_strategy": [int, 1], # default to REJECTION "seed": [int, -1], - "verbose": [int, 0], # level of verbosity, 0 = error, 1 = warn, 2 = info, 3 = debug + "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.], + "timeout": [float, -1.0], "fcstrs": [list, []], "fcstr_specs": [list, []], } @@ -115,9 +115,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): if self.unconstrained: m = 0 else: - indices, blc, buc, fact = self.optProb.getOrdering( - ["ne", "le", "ni", "li"], oneSided=True, noEquality=True - ) + indices, blc, buc, fact = self.optProb.getOrdering(["ne", "le", "ni", "li"], oneSided=True, noEquality=True) m = len(indices) self.optProb.jacIndices = indices self.optProb.fact = fact @@ -127,10 +125,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): 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) - ] + xspecs = [egobox.XSpec(egobox.XType.FLOAT, [float(blx[i]), float(bux[i])]) for i in range(n)] gp_config = opt("gp_config") if gp_config is None: @@ -162,7 +157,9 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): 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.") + raise ValueError( + "Option 'fcstr_specs' length must be zero or match the number of function constraints." + ) ctor_supported = set(inspect.signature(egobox.Egor).parameters.keys()) supports_ctor_verbose = "verbose" in ctor_supported diff --git a/tests/test_egor.py b/tests/test_egor.py index fbb6ebaf7..42c6c5199 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -4,24 +4,24 @@ import tempfile # First party modules -from pyoptsparse import OPT, Optimization +from pyoptsparse import Optimization from pyoptsparse.testing import OptTest import egobox as egx 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 = {} @@ -47,18 +47,18 @@ 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, int(egx.ExitStatus.MAX_ITERS_REACHED)) + self.assert_inform_equal(sol, int(egx.ExitStatus.MAX_ITERS_REACHED)) # Test that the inform is "Target function value reached" - sol = self.optimize(optOptions={"target": -10.}) - self.assert_inform_equal(sol, int(egx.ExitStatus.TARGET_COST_REACHED)) + sol = self.optimize(optOptions={"target": -10.0}) + self.assert_inform_equal(sol, int(egx.ExitStatus.TARGET_COST_REACHED)) # Test that the inform is "Time limit reached" sol = self.optimize(optOptions={"timeout": 1e-6}) self.assert_inform_equal(sol, int(egx.ExitStatus.TIMEOUT)) def test_egor_warm_start(self): - with tempfile.TemporaryDirectory() as outdir: + 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}) @@ -70,21 +70,28 @@ def test_egor_warm_start(self): self.assertGreater(sol1.fStar, sol2.fStar) def test_egor_config(self): - with tempfile.TemporaryDirectory() as outdir: + with tempfile.TemporaryDirectory() as outdir: self.setup_xsinx_optProb() # Test that the gp_config option is passed correctly gp_config = {"corr_spec": 4} # Matern 3/2 - sol = self.optimize(optOptions={"infill_strategy": 1, "gp_config": gp_config, - "outdir": outdir, "trego": {"n_gl_steps": (1, 3)}}) + _ = 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 import json + with open(f"{outdir}/egor_config.json", "r") as f: egor_config = json.load(f) print("Egor config: ", egor_config) self.assertEqual(egor_config["gp"]["correlation_spec"], "MATERN32") 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]) + self.assertEqual(egor_config["iteration_strategy"]["n_gl_steps"], [1, 3]) def test_egor_ackley(self): """ @@ -94,9 +101,12 @@ def test_egor_ackley(self): 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 + 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 @@ -105,15 +115,19 @@ def objfunc(xdict): 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}) + 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, + } + ) # Check Solution print("Ackley Solution: ", sol.xStar, "f: ", sol.fStar) self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=1e-2) - diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 9cea3be14..9ac12f964 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -40,10 +40,7 @@ class TestHS71(OptTest): "DELFUN": 1e-10, "DABFUN": 1e-10, }, - "Egor" : { - "max_iters": 100, - "seed": 42 - }, + "Egor": {"max_iters": 100, "seed": 42}, } def objfunc(self, xdict): From 7c814e46c4ca1802c70e4fc54ea8dc4a1c6e2597 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 14:24:14 +0200 Subject: [PATCH 10/33] Add test with constraints --- tests/test_egor.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/test_egor.py b/tests/test_egor.py index 42c6c5199..dd7df77e3 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -61,7 +61,7 @@ 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}) + sol1 = self.optimize(optOptions={"max_iters": 1, "outdir": outdir, "seed": 0}) print("First run: ", sol1.xStar, "f: ", sol1.fStar) # Second run with warm start sol2 = self.optimize(optOptions={"max_iters": 5, "outdir": outdir, "warm_start": True}) @@ -73,7 +73,7 @@ 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} # Matern 3/2 + gp_config = {"corr_spec": 4, "kpls_dim": 1} _ = self.optimize( optOptions={ "infill_strategy": 1, @@ -89,6 +89,7 @@ def test_egor_config(self): egor_config = json.load(f) print("Egor config: ", egor_config) 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]) @@ -131,3 +132,43 @@ def objfunc(xdict): self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=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 + print("G24 Solution: ", sol.xStar, "f: ", sol.fStar) + self.assertLess(sol.fStar, -5.50) # Should find a value close to -5.5080 + self.assertAlmostEqual(sol.xStar["xvars"][0], 2.3295, delta=0.1) + self.assertAlmostEqual(sol.xStar["xvars"][1], 3.1785, delta=0.1) From e1b19eb6076dc5d396d98d812973b73f0c77b72b Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 14:54:14 +0200 Subject: [PATCH 11/33] Fix doc --- doc/index.rst | 2 +- doc/optimizers/Egor.rst | 21 +++++++++++++---- doc/optimizers/Egor_options.yaml | 39 +++++++++++++++++++++++--------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index d1b5d9887..ba2a71753 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -69,5 +69,5 @@ To get started, please see the :ref:`install` and the :ref:`quickstart`. optimizers/ParOpt optimizers/CONMIN optimizers/ALPSO - optimizers/Egor optimizers/UNO + optimizers/Egor diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index 2cfd35d9d..eae13546e 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -4,11 +4,7 @@ Egor ==== Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided by the -`egobox `_ package which is intalled with: - -.. prompt:: bash - - pip install egobox +`egobox `_. Egor uses `bayesian optimization `_ techniques well-suited to find the global optimum of an expansive-to-evaluate black-box function. @@ -19,6 +15,16 @@ 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:: bash + + pip install egobox + + Options ------- @@ -39,6 +45,11 @@ Names and default values of the fields are provided in the descriptions below. .. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor :filename: Egor_options.yaml +Informs +------- +.. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor + :type: informs + API --- .. currentmodule:: pyoptsparse.pyEgor.pyEgor diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml index 8fe75cc08..8f19b1028 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -1,31 +1,47 @@ gp_config: - desc: GpConfig as a dict used by Egor for surrogate model configuration + 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) + 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 + 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 enum (EI, WB2, WB2S, LOG_EI) + desc: Infill criterion enum (EI=1, WB2=2, WB2S=3, LOG_EI=4) cstr_infill: - desc: Enable constrained infill criterion + desc: Enable constrained infill criterion (aka CEI) cstr_strategy: - desc: Constraint strategy enum for surrogate constraint handling + desc: | + Constraint strategy enum for surrogate constraint handling + - MeanConstraint=1 (default) + - UpperConfidenceBound=2 qei_config: desc: QEiConfig for batch (qEI) point selection infill_optimizer: - desc: Internal infill optimizer enum (COBYLA or SLSQP) + desc: | + Internal infill optimizer: + - COBYLA=1 (default) + - SLSQP=2 trego: desc: Enable TREGO (aka Trust Region EGO) algorithm coego_n_coop: - desc: Number of cooperative groups for CoEGO mode + desc: Number of cooperative groups for CoEGO algorithm target: desc: Known objective target used as stopping criterion outdir: - desc: Output directory for Egor history and warm start search + 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: @@ -37,13 +53,14 @@ failsafe_strategy: seed: desc: Seed for random number generator (default -1 for random seed) verbose: - desc: Constructor verbosity + desc: + Verbosity level for Egor logging (0=error, 1=warn, 2=info, 3=debug, 4=trace) max_iters: desc: Egor minimize iteration budget run_info: desc: Optional RunInfo used to pass additional information to Egor (e.g., for logging) timeout: - desc: Optional minimize timeout in seconds + desc: Optional timeout in seconds used as sttopping criterion for Egor minimize fcstrs: desc: Optional list of native Egobox function constraints passed to minimize fcstr_specs: From d916f19b53e8934be26d90cf58729ca4714ac7cd Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 16:21:17 +0200 Subject: [PATCH 12/33] Improve doc --- doc/optimizers/Egor.rst | 16 +------- doc/optimizers/Egor_options.yaml | 69 +++++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index eae13546e..16eb47511 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -3,8 +3,8 @@ Egor ==== -Egor is a surrogate-based Efficient Global Optimization (EGO) algorithm provided by the -`egobox `_. +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. @@ -44,15 +44,3 @@ Names and default values of the fields are provided in the descriptions below. .. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor :filename: Egor_options.yaml - -Informs -------- -.. optionstable:: pyoptsparse.pyEgor.pyEgor.Egor - :type: informs - -API ---- -.. currentmodule:: pyoptsparse.pyEgor.pyEgor - -.. autoclass:: Egor - :members: __call__ diff --git a/doc/optimizers/Egor_options.yaml b/doc/optimizers/Egor_options.yaml index 8f19b1028..0b497ecb6 100644 --- a/doc/optimizers/Egor_options.yaml +++ b/doc/optimizers/Egor_options.yaml @@ -2,14 +2,15 @@ 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) + + - ``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. + Default is ``1e-4`` for all constraints. n_start: desc: Number of infill optimization runs (best run selected) n_doe: @@ -19,23 +20,40 @@ doe: 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 enum (EI=1, WB2=2, WB2S=3, LOG_EI=4) + 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 - - MeanConstraint=1 (default) - - UpperConfidenceBound=2 + Constraint strategy enum for surrogate constraint handling: + + - 1 = ``MeanConstraint`` (default) + - 2 = ``UpperConfidenceBound`` qei_config: - desc: QEiConfig for batch (qEI) point selection + 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: - - COBYLA=1 (default) - - SLSQP=2 + + - 1 = ``COBYLA`` (default) + - 2 = ``SLSQP`` trego: - desc: Enable TREGO (aka Trust Region EGO) algorithm + 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: @@ -49,19 +67,32 @@ hot_start: 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 (REJECTION, IMPUTATION, VIABILITY) + 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, 1=warn, 2=info, 3=debug, 4=trace) + desc: | + Verbosity level for Egor logging + + - 0 = ``error`` (default) + - 1 = ``warning`` + - 2 = ``info`` + - 3 = ``debug`` max_iters: - desc: Egor minimize iteration budget + 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 to minimize + 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 + desc: | + Optional list of egobox.CstrSpec for function constraints passed as fcstrs From 2c99a3f43896a3afe79cdae6dbdba7e22edbd5e8 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 16:40:27 +0200 Subject: [PATCH 13/33] Cleanup --- uno_log_SILENT.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 uno_log_SILENT.txt diff --git a/uno_log_SILENT.txt b/uno_log_SILENT.txt deleted file mode 100644 index e69de29bb..000000000 From e2607df3b8da747d8caffb40e56d2d025cb88277 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 16:58:21 +0200 Subject: [PATCH 14/33] Do not import egobox for tests --- tests/test_egor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_egor.py b/tests/test_egor.py index dd7df77e3..977101ee2 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -7,8 +7,6 @@ from pyoptsparse import Optimization from pyoptsparse.testing import OptTest -import egobox as egx - class TestEgor(OptTest): def setup_xsinx_optProb(self): @@ -47,15 +45,15 @@ 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, int(egx.ExitStatus.MAX_ITERS_REACHED)) + 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, int(egx.ExitStatus.TARGET_COST_REACHED)) + 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, int(egx.ExitStatus.TIMEOUT)) + self.assert_inform_equal(sol, 5) def test_egor_warm_start(self): with tempfile.TemporaryDirectory() as outdir: From 75b74ebb301c0ef35aaad669c64d8af13fb23902 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 15 Jun 2026 17:06:21 +0200 Subject: [PATCH 15/33] Fix import order --- pyoptsparse/pyEgor/pyEgor.py | 2 +- tests/test_egor.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 801bcf1ec..8e67d17cb 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -11,8 +11,8 @@ import numpy as np # Local modules -from ..pyOpt_solution import SolutionInform from ..pyOpt_optimizer import Optimizer +from ..pyOpt_solution import SolutionInform from ..pyOpt_utils import import_module # import the Python module diff --git a/tests/test_egor.py b/tests/test_egor.py index 977101ee2..8a35678ef 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -1,8 +1,12 @@ """Test class for Egor specific tests""" -import numpy as np +# 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 @@ -81,8 +85,6 @@ def test_egor_config(self): } ) # read egor_config.json from outdir and check that corr_spec is 4 - import json - with open(f"{outdir}/egor_config.json", "r") as f: egor_config = json.load(f) print("Egor config: ", egor_config) From aa50911267ce8bd6c60e3408178dcfef13507250 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 09:27:32 +0200 Subject: [PATCH 16/33] Add egobox dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b3723001f..2f964ffdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ dependencies = [ "sqlitedict>=1.6", "numpy>=1.25", "scipy>=1.11", - "mdolab-baseclasses>=1.3.1" + "mdolab-baseclasses>=1.3.1", + "egobox>=0.37.6" ] dynamic = ["version"] # version is dynamically populated from meson project From 95deaee46e582b550d9f13c1741dc4ecb30157d1 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 09:29:09 +0200 Subject: [PATCH 17/33] Remove print statements --- tests/test_egor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_egor.py b/tests/test_egor.py index 8a35678ef..13c6b9066 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -42,7 +42,6 @@ def test_egor(self): self.setup_xsinx_optProb() sol = self.optimize() # Check Solution - print("xsinx Solution: ", sol.xStar, "f: ", sol.fStar) self.assertLess(sol.fStar, -15.1) # Should find a negative minimum def test_egor_inform(self): @@ -64,10 +63,8 @@ def test_egor_warm_start(self): self.setup_xsinx_optProb() # First run to generate a history file sol1 = self.optimize(optOptions={"max_iters": 1, "outdir": outdir, "seed": 0}) - print("First run: ", sol1.xStar, "f: ", sol1.fStar) # Second run with warm start sol2 = self.optimize(optOptions={"max_iters": 5, "outdir": outdir, "warm_start": True}) - print("Second run (warm start): ", sol2.xStar, "f: ", sol2.fStar) # Check that the second run continued from the first run self.assertGreater(sol1.fStar, sol2.fStar) @@ -87,7 +84,6 @@ def test_egor_config(self): # 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) - print("Egor config: ", egor_config) 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") @@ -128,7 +124,6 @@ def objfunc(xdict): } ) # Check Solution - print("Ackley Solution: ", sol.xStar, "f: ", sol.fStar) self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=1e-2) @@ -168,7 +163,6 @@ def objfunc(xdict): optOptions={"max_iters": 30, "n_doe": 5, "target": -5.50, "cstr_tol": [1e-3, 1e-3], "verbose": 2} ) # Check Solution - print("G24 Solution: ", sol.xStar, "f: ", sol.fStar) self.assertLess(sol.fStar, -5.50) # Should find a value close to -5.5080 self.assertAlmostEqual(sol.xStar["xvars"][0], 2.3295, delta=0.1) self.assertAlmostEqual(sol.xStar["xvars"][1], 3.1785, delta=0.1) From 090bbfda8f727cceba168ba7fb529e8a508994d5 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 10:18:15 +0200 Subject: [PATCH 18/33] Try TREGO to improve convergence on ackley test --- tests/test_egor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_egor.py b/tests/test_egor.py index 13c6b9066..214074b2e 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -121,6 +121,7 @@ def objfunc(xdict): "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 From 109f7db1626d9da2d674e1de88fda46176f13c5c Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 10:39:13 +0200 Subject: [PATCH 19/33] Add egobox in environment.yml --- .github/environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/environment.yml b/.github/environment.yml index 0411e61dd..f88ce7f87 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -18,6 +18,7 @@ dependencies: - scipy >=1.7 - sqlitedict >=1.6 - cyipopt + - egobox >=0.37.6 # testing - parameterized - testflo From 05abbbbf27486e479c7076b7eeb4cfae4c64428d Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 11:12:15 +0200 Subject: [PATCH 20/33] Try Egor conf to make tests pass on windows --- tests/test_hs015.py | 2 +- tests/test_hs071.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_hs015.py b/tests/test_hs015.py index 00c1240ec..9ac45f64e 100644 --- a/tests/test_hs015.py +++ b/tests/test_hs015.py @@ -53,7 +53,7 @@ class TestHS15(OptTest): optOptions = { "Egor": { "max_iters": 50, - "n_doe": 12, + "n_doe": 30, "seed": 42, } } diff --git a/tests/test_hs071.py b/tests/test_hs071.py index 9ac12f964..cb6b61b06 100644 --- a/tests/test_hs071.py +++ b/tests/test_hs071.py @@ -40,7 +40,7 @@ class TestHS71(OptTest): "DELFUN": 1e-10, "DABFUN": 1e-10, }, - "Egor": {"max_iters": 100, "seed": 42}, + "Egor": {"max_iters": 100, "seed": 42, "trego": {"n_gl_steps": (1, 4)}}, } def objfunc(self, xdict): From 27bf363a76b73333597955d03597e79eb0b97a7d Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 18:35:47 +0200 Subject: [PATCH 21/33] Add sphere test --- tests/test_sphere.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_sphere.py b/tests/test_sphere.py index ca8f132d4..b1ff5e33a 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -12,7 +12,7 @@ from pyoptsparse.pyOpt_optimizer import Optimizers from pyoptsparse.testing import OptTest -ALL_OPTIMIZERS = sorted({e.name for e in Optimizers} - {"ParOpt", "NSGA2", "Egor"}) +ALL_OPTIMIZERS = sorted({e.name for e in Optimizers} - {"ParOpt", "NSGA2"}) class TestSphere(OptTest): @@ -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}, } def objfunc(self, xdict): From 2014daff3355a1f49f87218a9ff04c02fcfa5f38 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 18:43:49 +0200 Subject: [PATCH 22/33] Use TREGO to improve test convergence --- tests/test_sphere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sphere.py b/tests/test_sphere.py index b1ff5e33a..dbe162435 100644 --- a/tests/test_sphere.py +++ b/tests/test_sphere.py @@ -61,7 +61,7 @@ class TestSphere(OptTest): "Major iterations limit": 10, }, "Uno": {"max_iterations": 100, "preset": "filtersqp"}, - "Egor": {"max_iters": 100, "seed": 123}, + "Egor": {"max_iters": 100, "seed": 123, "trego": {"n_gl_steps": (1, 4)}}, } def objfunc(self, xdict): From db61aa77765f0ca286a4b3a9f0ac0b7beb959806 Mon Sep 17 00:00:00 2001 From: relf Date: Tue, 16 Jun 2026 18:56:44 +0200 Subject: [PATCH 23/33] Relax test tolerance --- tests/test_egor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_egor.py b/tests/test_egor.py index 214074b2e..0b03cee12 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -42,7 +42,7 @@ def test_egor(self): self.setup_xsinx_optProb() sol = self.optimize() # Check Solution - self.assertLess(sol.fStar, -15.1) # Should find a negative minimum + self.assertLess(sol.fStar, -15.0) def test_egor_inform(self): self.setup_xsinx_optProb() From 7868382ec8eee0bd4a6ae238fe124002b59e8a6b Mon Sep 17 00:00:00 2001 From: relf Date: Sun, 21 Jun 2026 09:10:49 +0200 Subject: [PATCH 24/33] Move egobox in testing deps and use a release range --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2f964ffdc..d9b332a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ dependencies = [ "numpy>=1.25", "scipy>=1.11", "mdolab-baseclasses>=1.3.1", - "egobox>=0.37.6" ] dynamic = ["version"] # version is dynamically populated from meson project @@ -31,6 +30,7 @@ docs = [ testing = [ "testflo>=1.4.5", "parameterized", + "egobox>=0.37.6,<0.38.0", ] dev = [ "meson-python", From c7f3163275d92d5fbc20a8c90bc62f3d508251ea Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 29 Jun 2026 11:34:32 +0200 Subject: [PATCH 25/33] Mention conda install --- doc/index.rst | 2 +- doc/optimizers/Egor.rst | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index ba2a71753..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 `_. diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index 16eb47511..fd61e41cb 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -20,9 +20,13 @@ Installation Egor is made available through the `egobox `_ Python package. -.. prompt:: bash +.. prompt:: - pip install egobox + $ pip install egobox + +``egobox`` is also available via conda-forge:: + + $ conda install -c conda-forge egobox Options @@ -30,15 +34,15 @@ Options Please refer to the Egor help for a complete listing of options and their default values. -.. prompt:: bash +.. prompt:: - python + $ 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 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. From 2b26b2114999e13957cc49baae456a785eede5f4 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 29 Jun 2026 11:39:13 +0200 Subject: [PATCH 26/33] Remove trailing space --- doc/optimizers/Egor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/optimizers/Egor.rst b/doc/optimizers/Egor.rst index fd61e41cb..2559dd3bd 100644 --- a/doc/optimizers/Egor.rst +++ b/doc/optimizers/Egor.rst @@ -24,7 +24,7 @@ Egor is made available through the `egobox `_ $ pip install egobox -``egobox`` is also available via conda-forge:: +``egobox`` is also available via conda-forge:: $ conda install -c conda-forge egobox From d671ede7ebb8113a7f1dd09d5bbd75cf4bb55c60 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 08:09:35 +0200 Subject: [PATCH 27/33] Move egobox in testing section --- .github/environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/environment.yml b/.github/environment.yml index f88ce7f87..577c99079 100644 --- a/.github/environment.yml +++ b/.github/environment.yml @@ -18,7 +18,7 @@ dependencies: - scipy >=1.7 - sqlitedict >=1.6 - cyipopt - - egobox >=0.37.6 # testing - parameterized - testflo + - egobox >=0.37.6, <0.38.0 From 45687fb8f5a9eefdadd8d6f6695fbd810ebfe25e Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 09:27:21 +0200 Subject: [PATCH 28/33] Use assert_solution_allclose --- tests/test_egor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_egor.py b/tests/test_egor.py index 0b03cee12..fa46dddf1 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -125,9 +125,9 @@ def objfunc(xdict): } ) # Check Solution - self.assertAlmostEqual(sol.fStar, 0.0, delta=1e-2) - self.assertAlmostEqual(sol.xStar["xvars"][0], 0.0, delta=1e-2) - self.assertAlmostEqual(sol.xStar["xvars"][1], 0.0, delta=1e-2) + self.fStar = [0.0] + self.xStar = [{"xvars": (0.0, 0.0)},] + self.assert_solution_allclose(sol, tol=1e-2) def test_egor_g24(self): """ @@ -164,6 +164,6 @@ def objfunc(xdict): optOptions={"max_iters": 30, "n_doe": 5, "target": -5.50, "cstr_tol": [1e-3, 1e-3], "verbose": 2} ) # Check Solution - self.assertLess(sol.fStar, -5.50) # Should find a value close to -5.5080 - self.assertAlmostEqual(sol.xStar["xvars"][0], 2.3295, delta=0.1) - self.assertAlmostEqual(sol.xStar["xvars"][1], 3.1785, delta=0.1) + self.fStar = [-5.5080] + self.xStar = [{"xvars": (2.3295, 3.1785)},] + self.assert_solution_allclose(sol, tol=1e-2) From 96e34ddaa046f68a44b29f63dead668bf1d16fa0 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 09:31:55 +0200 Subject: [PATCH 29/33] Initialize callCounter --- pyoptsparse/pyEgor/pyEgor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 8e67d17cb..ec515bba2 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -88,7 +88,8 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): 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() From 01a078da4f3755dea68f7ddabb3cd3c186d00530 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 09:52:45 +0200 Subject: [PATCH 30/33] Suppress redondant default handling --- pyoptsparse/pyEgor/pyEgor.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index ec515bba2..7f8832b80 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -89,7 +89,6 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): """ self.startTime = time.time() self.callCounter = 0 - # Save the optimization problem and finalize constraint Jacobian self.optProb = optProb self.optProb.finalize() @@ -114,10 +113,10 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): raise ValueError("Egor requires finite lower and upper bounds for all design variables.") if self.unconstrained: - m = 0 + n_cstr = 0 else: indices, blc, buc, fact = self.optProb.getOrdering(["ne", "le", "ni", "li"], oneSided=True, noEquality=True) - m = len(indices) + n_cstr = len(indices) self.optProb.jacIndices = indices self.optProb.fact = fact self.optProb.offset = buc @@ -129,31 +128,13 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): xspecs = [egobox.XSpec(egobox.XType.FLOAT, [float(blx[i]), float(bux[i])]) for i in range(n)] gp_config = opt("gp_config") - if gp_config is None: - gp_config = egobox.GpConfig() - infill_strategy = opt("infill_strategy") - if infill_strategy is None: - infill_strategy = egobox.InfillStrategy.LOG_EI - cstr_strategy = opt("cstr_strategy") - if cstr_strategy is None: - cstr_strategy = egobox.ConstraintStrategy.MC - qei_config = opt("qei_config") - if qei_config is None: - qei_config = egobox.QEiConfig() - infill_optimizer = opt("infill_optimizer") - if infill_optimizer is None: - infill_optimizer = egobox.InfillOptimizer.COBYLA - failsafe_strategy = opt("failsafe_strategy") - if failsafe_strategy is None: - failsafe_strategy = egobox.FailsafeStrategy.REJECTION fcstrs_opt = opt("fcstrs") - n_cstr = m fcstr_specs = opt("fcstr_specs") n_fcstrs = 0 if fcstrs_opt is None else len(fcstrs_opt) @@ -190,7 +171,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): def fun(x): x_eval = np.atleast_2d(np.asarray(x, dtype=float)) - ncols = 1 + m + 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) @@ -199,7 +180,7 @@ def fun(x): y[i, :] = np.nan continue y[i, 0] = float(np.atleast_1d(fobj)[0]) - if m > 0: + if n_cstr > 0: y[i, 1:] = np.asarray(fcon, dtype=float) return y From 45d12e1858c52e12b8a58c7e6bf87b08fc3e0efa Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 10:04:44 +0200 Subject: [PATCH 31/33] Cleanup egobox < 0.37.6 handling --- pyoptsparse/pyEgor/pyEgor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 7f8832b80..86c4a063f 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -143,12 +143,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "Option 'fcstr_specs' length must be zero or match the number of function constraints." ) - ctor_supported = set(inspect.signature(egobox.Egor).parameters.keys()) - supports_ctor_verbose = "verbose" in ctor_supported - - if opt("verbose") is not None and not supports_ctor_verbose: - raise ValueError("Installed egobox version does not support constructor option 'verbose'.") - + # Prepare the constructor kwargs for Egor. ctor_kwargs = { "gp_config": gp_config, "n_cstr": n_cstr, @@ -166,7 +161,6 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "target": float(opt("target")), "failsafe_strategy": failsafe_strategy, } - ctor_kwargs = {k: v for k, v in ctor_kwargs.items() if k in ctor_supported} solver = egobox.Egor(xspecs, **ctor_kwargs) def fun(x): @@ -186,6 +180,7 @@ def fun(x): 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, From 50dd5e9b7d712ccf994331287b8d9415452fefa3 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 10:14:35 +0200 Subject: [PATCH 32/33] Add docstrings --- pyoptsparse/pyEgor/pyEgor.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index 86c4a063f..c31d043bd 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -82,6 +82,26 @@ 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. @@ -89,6 +109,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): """ self.startTime = time.time() self.callCounter = 0 + # Save the optimization problem and finalize constraint Jacobian self.optProb = optProb self.optProb.finalize() @@ -112,6 +133,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): 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: @@ -163,6 +185,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): } 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 From aea288e49b27ff467ee1d534ac8d64ed728d3392 Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 6 Jul 2026 10:18:54 +0200 Subject: [PATCH 33/33] Format --- pyoptsparse/pyEgor/pyEgor.py | 7 +++---- tests/test_egor.py | 10 +++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pyoptsparse/pyEgor/pyEgor.py b/pyoptsparse/pyEgor/pyEgor.py index c31d043bd..fbe23a1d9 100644 --- a/pyoptsparse/pyEgor/pyEgor.py +++ b/pyoptsparse/pyEgor/pyEgor.py @@ -4,7 +4,6 @@ # Standard Python modules import datetime -import inspect import time # External modules @@ -109,7 +108,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): """ self.startTime = time.time() self.callCounter = 0 - + # Save the optimization problem and finalize constraint Jacobian self.optProb = optProb self.optProb.finalize() @@ -165,7 +164,7 @@ def __call__(self, optProb, storeHistory=None, hotStart=None, **kwargs): "Option 'fcstr_specs' length must be zero or match the number of function constraints." ) - # Prepare the constructor kwargs for Egor. + # Prepare the constructor kwargs for Egor. ctor_kwargs = { "gp_config": gp_config, "n_cstr": n_cstr, @@ -203,7 +202,7 @@ def fun(x): fcstrs = [] if fcstrs_opt is None else list(fcstrs_opt) - # Prepare the minimize kwargs for Egor minimize. + # Prepare the minimize kwargs for Egor minimize. minimize_kwargs = { "fcstrs": fcstrs, "fcstr_specs": [] if fcstr_specs is None else fcstr_specs, diff --git a/tests/test_egor.py b/tests/test_egor.py index fa46dddf1..6a9803c19 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -126,8 +126,10 @@ def objfunc(xdict): ) # Check Solution self.fStar = [0.0] - self.xStar = [{"xvars": (0.0, 0.0)},] - self.assert_solution_allclose(sol, tol=1e-2) + self.xStar = [ + {"xvars": (0.0, 0.0)}, + ] + self.assert_solution_allclose(sol, tol=1e-2) def test_egor_g24(self): """ @@ -165,5 +167,7 @@ def objfunc(xdict): ) # Check Solution self.fStar = [-5.5080] - self.xStar = [{"xvars": (2.3295, 3.1785)},] + self.xStar = [ + {"xvars": (2.3295, 3.1785)}, + ] self.assert_solution_allclose(sol, tol=1e-2)