From 3fd3cd0cb6c65ae6b14f0ff6b331c45f7af0781f Mon Sep 17 00:00:00 2001 From: relf Date: Mon, 28 Sep 2026 17:57:07 +0200 Subject: [PATCH 1/2] Update wrapper against egobox 0.38 --- docs/src/performant_algs/egor.md | 90 +++++++--- modopt/external_libraries/egobox/egor.py | 215 +++++++++++++++++------ pyproject.toml | 6 +- tests/test_egor.py | 104 ++++++++++- 4 files changed, 331 insertions(+), 84 deletions(-) diff --git a/docs/src/performant_algs/egor.md b/docs/src/performant_algs/egor.md index 1a3167d..7eff54d 100644 --- a/docs/src/performant_algs/egor.md +++ b/docs/src/performant_algs/egor.md @@ -16,7 +16,7 @@ Egor in modOpt currently requires finite lower and upper bounds on every design Problems with unbounded variables are rejected. ``` -Before using `Egor`, install `egobox`: +Before using `Egor`, install `egobox` (version 0.38 or later): ```sh pip install egobox @@ -43,7 +43,9 @@ optimizer = Egor(prob, solver_options={"max_iters": 50, "n_doe": 10, "seed": 42} modOpt forwards Egor options through two paths: - constructor options are passed to `egobox.Egor(...)` -- runtime options are passed to `Egor.minimize(...)` +- runtime options (`max_iters`, `seed`, `outdir`, `warm_start`, `hot_start`, + `run_info`, `timeout`, `verbose`, `stop_on_error`, `fcstrs`, `fcstr_specs`) + are passed to `Egor.minimize(...)` ```{note} For constrained problems, do not pass `solver_options['cstr_specs']` directly. @@ -62,13 +64,16 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. - Maximum number of Egor iterations. Passed to \ `minimize()`. * - `gp_config` - - *egobox.GpConfig* + - *egobox.GpConfig*, *dict*, or `None` (`None`) - GP configuration used by the optimizer, see \ - GpConfig for details. -* - `n_start` - - *int* (`20`) + GpConfig for details. When `None`, the egobox \ + default is used. +* - `infill_n_start` + - *int* or `None` (`None`) - Number of runs of infill strategy optimizations; \ - the best result is taken. + the best result is taken. When `None`, the \ + egobox default is used. Replaces the deprecated \ + `n_start` option. * - `n_doe` - *int* (`0`) - Number of samples of initial LHS sampling, used \ @@ -76,17 +81,29 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. the number of points is computed automatically \ regarding the number of input variables of the \ function under optimization. -* - `doe` +* - `x_doe` + - *None*, *list*, *tuple*, or *ndarray* (`None`) + - Initial DOE inputs, shape `(ns, nx)`. When \ + `y_doe` is not given, the `ns` points are \ + evaluated first. Replaces the deprecated `doe` \ + option (`doe[:, :nx]`). +* - `y_doe` - *None*, *list*, *tuple*, or *ndarray* (`None`) - - Initial DOE containing `ns` samples. Either \ - `nt = nx` then only `x` is specified and `ns` \ - evaluations are done to get `y_doe` values, or \ - `nt = nx + ny` then `x = doe[:, :nx]` and \ - `y = doe[:, nx:]` are provided. + - Outputs at `x_doe`, shape `(ns, 1 + n_cstr)`: \ + the objective, then the raw values of the \ + modOpt constraints with at least one finite \ + bound, in order. Requires `x_doe`. * - `infill_strategy` - *egobox.InfillStrategy* (`LOG_EI`) - Infill criterion used to decide the next \ promising point. +* - `feasible_infill_strategy` + - *egobox.FeasibleInfillStrategy* (`NONE`) + - Strategy to take feasibility into account in the \ + infill criterion (Expected Feasible Improvement): \ + `NONE`, `EFI_P`, or `EFI_FE`. `EFI_P` and \ + `EFI_FE` require `infill_strategy` to be `EI`, \ + `WB2`, or `WB2S`. * - `cstr_infill` - *bool* (`False`) - Activates the constrained infill criterion, \ @@ -97,7 +114,7 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. - Constraint management strategy for infill; use \ the mean value or the upper trusted bound. * - `qei_config` - - *egobox.QEiConfig* + - *egobox.QEiConfig*, *dict*, or `None` (`None`) - Configuration for parallel qEI, also known as \ batch or multipoint evaluation. `q` points are \ selected at each iteration of the EGO algorithm. @@ -114,11 +131,14 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. - Number of cooperative component groups used by \ the CoEGO algorithm. * - `target` - - *float* (`-max_float`) - - Known optimum used as a stopping criterion. + - *float* or `None` (`None`) + - Known optimum used as a stopping criterion. \ + When `None`, no target is used. * - `failsafe_strategy` - *egobox.FailsafeStrategy* (`REJECTION`) - - Strategy to handle objective computation failure. + - Strategy to handle objective computation failure \ + (NaN values or errors): `REJECTION`, \ + `IMPUTATION`, or `VIABILITY`. * - `seed` - *int* or `None` (`None`) - Random generator seed to allow computation \ @@ -131,7 +151,7 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. - *bool* (`False`) - Start by loading initial DOE from `outdir`. * - `hot_start` - - *int* or `None` (`None`) + - *bool*, *int*, or `None` (`None`) - When `True`, `hot_start` behaves like \ `hot_start = 0` with no iteration extension. \ When `hot_start >= 0`, the optimizer state is \ @@ -147,15 +167,29 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. stops when the elapsed time exceeds this \ duration. * - `verbose` - - *int*, *egobox.Verbosity*, or `None` (`None`) + - *int*, *egobox.Verbose*, or `None` (`None`) - Logging verbosity level. Default is `None`, \ which means `Verbose.ERROR` and possible \ control by the `EGOBOX_LOG` environment \ variable. -* - `cstr_tol` - - *None*, *list*, *tuple*, or *ndarray* (`None`) - - List of tolerances for constraints to be \ - satisfied (`cstr < tol`). +* - `stop_on_error` + - *bool* (`False`) + - If `True`, terminate the optimization when the \ + objective function raises an error. Otherwise, \ + the error is handled according to \ + `failsafe_strategy`. +* - `cstr_tols` + - *None*, *float*, *list*, *tuple*, or *ndarray* \ + (`None`) + - Constraint violation tolerances: a scalar, or one \ + value per modOpt constraint with at least one \ + finite bound. Each one is set as the tolerance \ + of the corresponding `CstrSpec`. Use `CstrSpec` \ + tolerances in `fcstr_specs` for function \ + constraints. For equality and double-sided \ + constraints, the single value applies to both \ + internal `c(x) <= 0` constraints. Replaces the \ + deprecated `cstr_tol` option. * - `cstr_specs` - *None*, *list*, or *tuple* (`None`) - Optional list of `CstrSpec` objects describing \ @@ -170,10 +204,20 @@ The modOpt wrapper builds `cstr_specs` automatically from `cl` and `cu`. function constraint. ``` +```{note} +`n_start`, `doe` and `cstr_tol` are deprecated, use `infill_n_start`, `x_doe`/`y_doe` +and `cstr_tols` instead. +They still work but emit a `DeprecationWarning`. +``` + +In the results, `y_doe` holds the objective followed by the raw values of the +modOpt constraints with at least one finite bound, as for the `y_doe` option. + ```{note} Detailed information on `egobox` objects can be retrieved using the python interpreter. See example below. ``` ```bash > python +>>> import egobox >>> help(egobox.GpConfig) ``` diff --git a/modopt/external_libraries/egobox/egor.py b/modopt/external_libraries/egobox/egor.py index e05c758..dd75614 100644 --- a/modopt/external_libraries/egobox/egor.py +++ b/modopt/external_libraries/egobox/egor.py @@ -1,5 +1,5 @@ import time -import inspect +import warnings import numpy as np @@ -39,13 +39,16 @@ class Egor(Optimizer): solver_options : dict, default={} Dictionary containing the options to be passed to the Egor solver. - Supported options are ``'max_iters'``, ``'gp_config'``, ``'n_cstr'``, ``'n_start'``, - ``'n_doe'``, ``'doe'``, ``'infill_strategy'``, ``'cstr_infill'``, - ``'cstr_strategy'``, ``'qei_config'``, ``'infill_optimizer'``, + Supported options are ``'max_iters'``, ``'gp_config'``, ``'n_cstr'``, + ``'infill_n_start'``, ``'n_doe'``, ``'x_doe'``, ``'y_doe'``, ``'infill_strategy'``, + ``'feasible_infill_strategy'``, + ``'cstr_infill'``, ``'cstr_strategy'``, ``'qei_config'``, ``'infill_optimizer'``, ``'trego'``, ``'coego_n_coop'``, ``'target'``, ``'outdir'``, ``'warm_start'``, ``'hot_start'``, ``'failsafe_strategy'``, - ``'seed'``, ``'cstr_tol'``, ``'run_info'``, ``'timeout'``, ``'verbose'``, - ``'fcstrs'``, and ``'fcstr_specs'``. + ``'seed'``, ``'cstr_tols'``, ``'run_info'``, ``'timeout'``, ``'verbose'``, + ``'stop_on_error'``, ``'fcstrs'``, and ``'fcstr_specs'``. + ``'n_start'``, ``'doe'`` and ``'cstr_tol'`` are deprecated aliases of + ``'infill_n_start'``, ``'x_doe'``/``'y_doe'`` and ``'cstr_tols'``. readable_outputs : list, default=[] List of outputs to be written to readable text output files. Available outputs are ``'x'`` and ``'obj'``. @@ -66,31 +69,39 @@ def initialize(self): self.default_solver_options = { "max_iters": (int, 20), - "gp_config": (object, egx.GpConfig()), + # Options defaulting to None are not forwarded, so egobox defaults apply. + "gp_config": (object, None), "n_cstr": (int, 0), - "n_start": (int, 20), + "infill_n_start": ((type(None), int), None), "n_doe": (int, 0), - "doe": ((type(None), list, tuple, np.ndarray), None), + "x_doe": ((type(None), list, tuple, np.ndarray), None), + "y_doe": ((type(None), list, tuple, np.ndarray), None), "infill_strategy": (object, egx.InfillStrategy.LOG_EI), + "feasible_infill_strategy": (object, egx.FeasibleInfillStrategy.NONE), "cstr_infill": (bool, False), "cstr_strategy": (object, egx.ConstraintStrategy.MC), - "qei_config": (object, egx.QEiConfig()), + "qei_config": (object, None), "infill_optimizer": (object, egx.InfillOptimizer.COBYLA), "trego": (object, None), "coego_n_coop": (int, 0), - "target": (float, -np.finfo(float).max), + "target": ((type(None), float), None), "outdir": ((type(None), str), None), "warm_start": (bool, False), - "hot_start": ((type(None), int), None), + "hot_start": ((type(None), bool, int), None), "failsafe_strategy": (object, egx.FailsafeStrategy.REJECTION), "seed": ((type(None), int), None), - "cstr_tol": ((type(None), list, tuple, np.ndarray), None), + "cstr_tols": ((type(None), float, int, list, tuple, np.ndarray), None), "cstr_specs": ((type(None), list, tuple), None), "run_info": ((type(None), object), None), "timeout": ((type(None), float, int), None), "verbose": ((type(None), int, object), None), + "stop_on_error": (bool, False), "fcstrs": ((list, tuple), []), "fcstr_specs": ((list, tuple), []), + # Deprecated aliases, kept for backward compatibility. + "n_start": ((type(None), int), None), + "doe": ((type(None), list, tuple, np.ndarray), None), + "cstr_tol": ((type(None), float, int, list, tuple, np.ndarray), None), } self.solver_options = OptionsDictionary() @@ -167,17 +178,18 @@ def _setup_constraints(self): if not np.isfinite(lower) and not np.isfinite(upper): continue - if np.isfinite(lower) and np.isfinite(upper): - if np.isclose(lower, upper): - spec = self.egx.CstrSpec.eq(float(lower)) - else: - spec = self.egx.CstrSpec.btw(float(lower), float(upper)) - elif np.isfinite(upper): - spec = self.egx.CstrSpec.leq(float(upper)) - else: - spec = self.egx.CstrSpec.geq(float(lower)) + self._constraint_specs.append( + {"index": index, "lower": float(lower), "upper": float(upper)} + ) - self._constraint_specs.append({"index": index, "spec": spec}) + def _make_cstr_spec(self, lower, upper, tol): + if np.isfinite(lower) and np.isfinite(upper): + if np.isclose(lower, upper): + return self.egx.CstrSpec.eq(lower, tol=tol) + return self.egx.CstrSpec.between(lower, upper, tol=tol) + if np.isfinite(upper): + return self.egx.CstrSpec.leq(upper, tol=tol) + return self.egx.CstrSpec.geq(lower, tol=tol) def _normalize_solver_options(self): user_n_cstr = self.options_to_pass.get("n_cstr", 0) @@ -196,11 +208,28 @@ def _normalize_solver_options(self): "Constraint specs are generated automatically from problem.c_lower/c_upper." ) - if self.options_to_pass["doe"] is not None: - self.options_to_pass["doe"] = np.asarray( - self.options_to_pass["doe"], dtype=float + self._handle_deprecated_options() + + feasible_infill = self.options_to_pass["feasible_infill_strategy"] + if ( + feasible_infill != self.egx.FeasibleInfillStrategy.NONE + and self.options_to_pass["infill_strategy"] == self.egx.InfillStrategy.LOG_EI + ): + raise ValueError( + "solver_options['feasible_infill_strategy'] is not supported with the LOG_EI " + "infill_strategy. Use InfillStrategy.EI, WB2 or WB2S instead." ) + for option_name in ("x_doe", "y_doe"): + if self.options_to_pass[option_name] is not None: + self.options_to_pass[option_name] = np.atleast_2d( + np.asarray(self.options_to_pass[option_name], dtype=float) + ) + if self.options_to_pass["y_doe"] is not None and self.options_to_pass["x_doe"] is None: + raise ValueError("solver_options['y_doe'] requires solver_options['x_doe'].") + + cstr_tols = self._normalize_cstr_tols() + if self.problem.constrained: computed_n_cstr = len(self._constraint_specs) if user_n_cstr not in (0, computed_n_cstr): @@ -209,7 +238,8 @@ def _normalize_solver_options(self): ) self.options_to_pass["cstr_specs"] = [ - spec["spec"] for spec in self._constraint_specs + self._make_cstr_spec(spec["lower"], spec["upper"], tol) + for spec, tol in zip(self._constraint_specs, cstr_tols) ] self.options_to_pass["n_cstr"] = computed_n_cstr elif user_cstr_specs is None: @@ -220,20 +250,80 @@ def _normalize_solver_options(self): self.options_to_pass.pop("cstr_specs", None) self.options_to_pass["n_cstr"] = 0 - cstr_tol = self.options_to_pass["cstr_tol"] - if cstr_tol is not None: - if not self._constraint_specs: + def _normalize_cstr_tols(self): + # egobox >= 0.38 deprecates Egor(cstr_tol=...) in favor of a tolerance per + # CstrSpec, which applies to both internal constraints of eq/between specs. + # Function constraint tolerances are given through fcstr_specs. + cstr_tols = self.options_to_pass.pop("cstr_tols") + n_specs = len(self._constraint_specs) + if cstr_tols is None: + return [None] * n_specs + + if n_specs == 0: + raise ValueError( + "solver_options['cstr_tols'] was provided but the problem has no bounded constraints. " + "Use CstrSpec tolerances in solver_options['fcstr_specs'] for function constraints." + ) + + cstr_tols = np.asarray(cstr_tols, dtype=float).reshape(-1) + if cstr_tols.size == 1: + cstr_tols = np.full(n_specs, cstr_tols[0]) + if cstr_tols.size != n_specs: + raise ValueError( + f"solver_options['cstr_tols'] must be a scalar or have length {n_specs}, " + "one per modOpt constraint with at least one finite bound." + ) + return cstr_tols.tolist() + + def _handle_deprecated_options(self): + opts = self.options_to_pass + + n_start = opts.pop("n_start") + if n_start is not None: + warnings.warn( + "solver_options['n_start'] is deprecated, use 'infill_n_start' instead.", + DeprecationWarning, + stacklevel=4, + ) + if opts["infill_n_start"] is not None: raise ValueError( - "solver_options['cstr_tol'] was provided but the problem has no inequality constraints." + "Pass only one of solver_options['n_start'] and solver_options['infill_n_start']." ) + opts["infill_n_start"] = n_start - cstr_tol = np.asarray(cstr_tol, dtype=float).reshape(-1) - if cstr_tol.size != len(self._constraint_specs): + cstr_tol = opts.pop("cstr_tol") + if cstr_tol is not None: + warnings.warn( + "solver_options['cstr_tol'] is deprecated, use 'cstr_tols' instead.", + DeprecationWarning, + stacklevel=4, + ) + if opts["cstr_tols"] is not None: raise ValueError( - f"solver_options['cstr_tol'] must have length {len(self._constraint_specs)} for Egor after " - "converting the modOpt constraint bounds to c(x) <= 0 form." + "Pass only one of solver_options['cstr_tol'] and solver_options['cstr_tols']." ) - self.options_to_pass["cstr_tol"] = cstr_tol.tolist() + opts["cstr_tols"] = cstr_tol + + doe = opts.pop("doe") + if doe is not None: + warnings.warn( + "solver_options['doe'] is deprecated, use 'x_doe' and 'y_doe' instead.", + DeprecationWarning, + stacklevel=4, + ) + if opts["x_doe"] is not None or opts["y_doe"] is not None: + raise ValueError( + "Pass either solver_options['doe'] or solver_options['x_doe']/['y_doe'], not both." + ) + doe = np.atleast_2d(np.asarray(doe, dtype=float)) + nx = self.problem.nx + if doe.shape[1] < nx: + raise ValueError( + f"solver_options['doe'] must have at least {nx} columns (the design variables)." + ) + opts["x_doe"] = doe[:, :nx] + if doe.shape[1] > nx: + opts["y_doe"] = doe[:, nx:] def _egor_fun(self, x): # Egor can evaluate several candidate points at once. Convert batched inputs @@ -247,11 +337,17 @@ def _egor_fun(self, x): "Egor objective callback expects input with shape (n, nx) or (nx,)." ) - obj_vals = [self.obj(xi) for xi in x] + # Evaluate objective and constraints point by point: drivers (e.g. OpenMDAO's + # modOptDriver) cache constraints computed along with the objective at the + # last evaluated point only. + obj_vals, con_vals = [], [] + for xi in x: + obj_vals.append(self.obj(xi)) + if self.problem.constrained: + con_vals.append(np.asarray(self.con(xi), dtype=float).reshape((1, -1))) outputs = np.asarray(obj_vals, dtype=float).reshape((-1, 1)) if self.problem.constrained: - con_vals = [np.asarray(self.con(xi), dtype=float).reshape((1, -1)) for xi in x] con_block = np.vstack(con_vals) selected = con_block[:, [spec["index"] for spec in self._constraint_specs]] outputs = np.hstack((outputs, selected)) @@ -262,18 +358,25 @@ def solve(self): constructor_options = self.options_to_pass.copy() minimize_kwargs = {"max_iters": self.max_iters} - # Runtime-only controls are passed via minimize(). - # NOTE: Egobox accepts seed in the constructor, but this is deprecated upstream. - for option_name in ("run_info", "timeout", "seed"): + # Runtime controls are passed via minimize() (egobox >= 0.37). + for option_name in ( + "run_info", + "timeout", + "seed", + "outdir", + "warm_start", + "hot_start", + "verbose", + "stop_on_error", + ): value = constructor_options.pop(option_name, None) if value is not None: minimize_kwargs[option_name] = value - # Shared controls exist on both constructor and minimize in current Egobox API. - for option_name in ("outdir", "warm_start", "hot_start", "verbose"): - value = constructor_options.get(option_name, None) - if value is not None: - minimize_kwargs[option_name] = value + # Let egobox apply its own defaults for unset constructor options. + for option_name in ("gp_config", "qei_config", "infill_n_start", "target", "x_doe", "y_doe"): + if constructor_options.get(option_name) is None: + constructor_options.pop(option_name, None) # Function constraints are optional and independent of modOpt's grouped constraints. fcstrs = list(constructor_options.pop("fcstrs", [])) @@ -281,15 +384,7 @@ def solve(self): minimize_kwargs["fcstrs"] = fcstrs fcstr_specs = list(constructor_options.pop("fcstr_specs", [])) - - minimize_signature = inspect.signature(self.egx.Egor.minimize) - minimize_parameters = set(minimize_signature.parameters.keys()) if fcstr_specs: - if "fcstr_specs" not in minimize_parameters: - raise ValueError( - "solver_options['fcstr_specs'] was provided, but the installed egobox version " - "does not support the fcstr_specs argument in Egor.minimize()." - ) minimize_kwargs["fcstr_specs"] = fcstr_specs start_time = time.time() @@ -304,16 +399,22 @@ def solve(self): y_opt = np.asarray(egor_result.y_opt, dtype=float).reshape(-1) objective = float(y_opt[0]) + success = True message = "Optimization completed successfully." - if status is not None and hasattr(status, "exit"): - message = str(status.exit) + exit_status = getattr(status, "exit", None) + if exit_status is not None: + message = str(exit_status) + success = exit_status not in ( + self.egx.ExitStatus.UNEXPECTED_EXIT, + self.egx.ExitStatus.OBJECTIVE_FUNCTION_ERROR, + ) self.update_outputs(x=x_opt, obj=objective) self.results = { "x": x_opt, "fun": objective, - "success": True, + "success": success, "message": message, "x_doe": np.asarray(egor_result.x_doe, dtype=float), "y_doe": np.asarray(egor_result.y_doe, dtype=float), diff --git a/pyproject.toml b/pyproject.toml index cf7a3fe..147474c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,7 @@ cobyqa = [ "cobyqa", # cobyqa is already installed with scipy>=1.14.0, which requires python>=3.10 ] egobox = [ - "egobox" + "egobox>=0.38" ] pycutest = [ "pycutest", # Needs manual installation of the CUTEst library with the test problems @@ -157,7 +157,7 @@ interfaces = [ "osqp", # osqp MAY BE already installed with qpsolvers[open_source_solvers] "quadprog", # quadprog MAY BE already installed with qpsolvers[open_source_solvers] "cobyqa", # cobyqa is already installed with scipy>=1.14.0, which requires python>=3.10 - "egobox", + "egobox>=0.38", ] open_source_optimizers = [ "qpsolvers", @@ -167,7 +167,7 @@ open_source_optimizers = [ "osqp", # osqp MAY BE already installed with qpsolvers[open_source_solvers] "quadprog", # quadprog MAY BE already installed with qpsolvers[open_source_solvers] "cobyqa", # cobyqa is already installed with scipy>=1.14.0, which requires python>=3.10 - "egobox", + "egobox>=0.38", ] visualization = [ "matplotlib", diff --git a/tests/test_egor.py b/tests/test_egor.py index 0303475..5afdd63 100644 --- a/tests/test_egor.py +++ b/tests/test_egor.py @@ -285,7 +285,7 @@ def test_egor_g24_constrained_problem_lite(): "seed": 42, "infill_strategy": egx.InfillStrategy.WB2, "infill_optimizer": egx.InfillOptimizer.SLSQP, - "cstr_tol": [1e-3, 1e-3], + "cstr_tols": [1e-3, 1e-3], }, turn_off_outputs=True, ) @@ -296,6 +296,104 @@ def test_egor_g24_constrained_problem_lite(): assert results["fun"] <= -5.4 assert np.all(results["constraints"] <= 1e-3) + +def test_egor_cstr_tols_per_constraint(): + # One tolerance per modOpt constraint, also for equality constraints which + # expand to two internal constraints in Egor. + with pytest.raises(ValueError, match="scalar or have length 1"): + Egor( + EqualityConstraintProblem(), + solver_options={"cstr_tols": [1e-3, 1e-3]}, + turn_off_outputs=True, + ) + + for cstr_tols in (1e-2, [1e-2]): + optimizer = Egor( + EqualityConstraintProblem(), + solver_options={ + "max_iters": 15, + "n_doe": 5, + "seed": 13, + "cstr_tols": cstr_tols, + }, + turn_off_outputs=True, + ) + results = optimizer.solve() + assert results["success"] is True + assert_allclose(results["x"], [0.5], atol=1e-1) + + +def test_egor_initial_doe(): + x_doe = np.array([[-0.5], [0.0], [0.5], [0.9]]) + y_doe = (x_doe - 0.25) ** 2 + + optimizer = Egor( + finite_bounds_lite(), + solver_options={"max_iters": 10, "seed": 3, "x_doe": x_doe, "y_doe": y_doe}, + turn_off_outputs=True, + ) + results = optimizer.solve() + assert results["success"] is True + assert_allclose(results["x_doe"][:4], x_doe) + assert_allclose(results["x"], [0.25], atol=8e-2) + + +def test_egor_deprecated_options(): + x_doe = np.array([[-0.5], [0.0], [0.5], [0.9]]) + with pytest.warns(DeprecationWarning): + optimizer = Egor( + finite_bounds_lite(), + solver_options={"max_iters": 5, "seed": 3, "n_start": 10, "doe": x_doe}, + turn_off_outputs=True, + ) + assert optimizer.options_to_pass["infill_n_start"] == 10 + assert_allclose(optimizer.options_to_pass["x_doe"], x_doe) + assert optimizer.options_to_pass["y_doe"] is None + assert optimizer.solve()["success"] is True + + with pytest.warns(DeprecationWarning, match="cstr_tol"): + optimizer = Egor( + EqualityConstraintProblem(), + solver_options={"cstr_tol": [1e-2]}, + turn_off_outputs=True, + ) + assert optimizer.options_to_pass["cstr_specs"] is not None + + with pytest.raises(ValueError, match="only one"): + with pytest.warns(DeprecationWarning): + Egor( + finite_bounds_lite(), + solver_options={"n_start": 10, "infill_n_start": 10}, + turn_off_outputs=True, + ) + + +def test_egor_runtime_and_feasible_infill_options(): + # Feasible infill (EFI) is not implemented upstream for the default LOG_EI criterion. + with pytest.raises(ValueError, match="LOG_EI"): + Egor( + g24_lite(), + solver_options={"feasible_infill_strategy": egx.FeasibleInfillStrategy.EFI_P}, + turn_off_outputs=True, + ) + + optimizer = Egor( + g24_lite(), + solver_options={ + "max_iters": 10, + "n_doe": 5, + "seed": 42, + "infill_strategy": egx.InfillStrategy.EI, + "feasible_infill_strategy": egx.FeasibleInfillStrategy.EFI_P, + "stop_on_error": True, + "verbose": 0, + }, + turn_off_outputs=True, + ) + results = optimizer.solve() + assert results["success"] is True + assert np.isfinite(results["fun"]) + if __name__ == '__main__': test_egor_direct_interface() test_egor_with_inequality_constraints() @@ -304,4 +402,8 @@ def test_egor_g24_constrained_problem_lite(): test_egor_supports_equality_constraints() test_egor_with_upper_and_lower_ineq_constraints() test_egor_g24_constrained_problem_lite() + test_egor_cstr_tols_per_constraint() + test_egor_initial_doe() + test_egor_deprecated_options() + test_egor_runtime_and_feasible_infill_options() print('All tests passed!') \ No newline at end of file From f9235b0c8202787aa26cc5c14841ae3f0648dc29 Mon Sep 17 00:00:00 2001 From: relf Date: Thu, 1 Oct 2026 17:22:51 +0200 Subject: [PATCH 2/2] Bump version to 0.4.0 --- modopt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modopt/__init__.py b/modopt/__init__.py index 587be03..d957395 100644 --- a/modopt/__init__.py +++ b/modopt/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.3.1' +__version__ = '0.4.0' # import modopt base classes from modopt.core.optimizer import Optimizer