Cookbook🔗
This page contains practical Egor parameterization recipes.
Each recipe includes:
- when to use it
- a suggested configuration
- why it helps
Notes
- Start from the closest recipe, then tune one group of parameters at a time.
- Keep random seeds fixed while comparing configurations, and confirm a difference with at least two seeds: the seed alone can change the final objective by about 1%.
- Use a distinct
outdirfor each run you compare: withwarm_start=True, a run silently continues from the DOE already saved in itsoutdir. - Use the
timeoutargument ofminimize()to stop gracefully before a batch-job time limit. - Regarding the required
xspecsparameter (i.e. the input space) used byEgor: When using continuous variables only, the simplest form is a list of[lower, upper]pairs, e.g.[[0.0, 1.0], [1.0, 10.0], [-5.0, 5.0]]. Otherwise, see the XSpecs in Python API for more complex variable types. - For complete parameter definitions, see the Python API.
Recipe 1: Cheap Low-Dimensional Objective🔗
Use when:
- dimension is low (for example 2 to 5)
- objective evaluation is cheap
- you can afford more iterations
Suggested setup:
optim = egx.Egor(
xspecs,
n_doe=30,
)
res = optim.minimize(fun, max_iters=60, seed=42)
Why it helps:
- larger DOE improves global coverage early
- cheap evaluations allow more exploration iterations
Recipe 2: Expensive Objective🔗
Use when:
- each objective call is expensive (seconds to minutes)
- you need strong information gain at each iteration
Suggested setup:
optim = egx.Egor(
xspecs,
n_doe=13,
)
res = optim.minimize(fun, max_iters=20, seed=42)
Why it helps:
- smaller DOE reduces initial expensive calls (default xdim + 1)
Note:
- 20 iterations suit unconstrained problems in low dimension. With several constraints active at the optimum, plan for many more iterations (see Recipe 12, where about 100 to 200 iterations were needed in dimension 11).
Recipe 3: High Dimension (d > 10)🔗
Use when:
- the number of variables is high
Suggested setup:
gp_cfg = egx.GpConfig(kpls_dim=10)
optim = egx.Egor(
xspecs,
gp_config=gp_cfg,
)
res = optim.minimize(fun, max_iters=40, seed=42)
Why it helps:
- KPLS reduces effective surrogate complexity
- improves robustness in high-dimensional GP fitting
Rule of thumb:
- around d=20, start with kpls_dim=5
- around d=100, start with kpls_dim=10
- just above d=10 (up to about 15), first try without KPLS: full GPs remain affordable and can model constraints much more accurately than their KPLS counterparts
Recipe 4: Very High Dimension (d > 50)🔗
Use when:
- standard optimization stalls in very high dimension
Suggested setup:
gp_cfg = egx.GpConfig(kpls_dim=10)
optim = egx.Egor(
xspecs,
gp_config=gp_cfg,
coego_n_coop=5,
)
res = optim.minimize(fun, max_iters=60, seed=42)
Why it helps:
- CoEGO decomposes search into cooperative component groups
- better scaling behavior for very high dimension
Recipe 5: Parallel Evaluations Available🔗
Use when:
- you can evaluate multiple points concurrently
Suggested setup:
qei_cfg = egx.QEiConfig(batch=10, strategy=egx.QEiStrategy.KB, optim_every=1)
optim = egx.Egor(
xspecs,
qei_config=qei_cfg,
)
res = optim.minimize(fun, max_iters=20, seed=42)
Why it helps:
- qEI proposes batches of points per iteration
- better wall-clock performance on parallel hardware
Rule of thumb:
- around d=50, try batch=5
- around d=100, try batch=10
Recipe 6: Stagnation or Poor Progress🔗
Use when:
- best objective value barely improves
- optimizer revisits similar regions
Suggested setup:
gp_cfg = egx.GpConfig(corr_spec=egx.CorrelationSpec.MATERN52)
optim = egx.Egor(
xspecs,
gp_config=gp_cfg,
trego=True,
infill_strategy=egx.InfillStrategy.WB2,
infill_optimizer=egx.InfillOptimizer.SLSQP
)
res = optim.minimize(fun, max_iters=40, seed=42)
Why it helps:
- TREGO alternates global and local trust-region behavior improves convergence
- Matern52 is often more robust on rougher landscapes
- On bad infill optimization you can try to change the infill optimizer; try
SLSQP - Default
LOG_EIoptimization on rough landscapes may be too difficult; tryWB2,WB2Sor evenEIinstead. The effect is problem dependent: on a constrained 11D problem,WB2Sdid much worse thanLOG_EI(see Recipe 12), so compare withLOG_EIbefore switching.
Recipe 7: Constraint-Heavy Problems🔗
Use when:
- feasibility dominates the search difficulty
Suggested setup:
optim = egx.Egor(
xspecs,
n_cstr=n_cstr,
cstr_infill=True,
cstr_strategy=egx.ConstraintStrategy.UTB,
infill_strategy=egx.InfillStrategy.WB2,
feasible_infill_strategy=egx.FeasibleInfillStrategy.EFI_FE,
)
res = optim.minimize(fun, max_iters=30, seed=42)
Why it helps:
- UTB makes constraint handling more conservative under uncertainty
- EFI_FE increases exploration of feasible regions
Note🔗
- EFI_FE is not implemented for default infill strategies (LOG_EI), so you need to use WB2, EI or WB2S.
Recipe 8: Objective May Crash🔗
Use when:
- objective occasionally fails or returns
NaN - long runs may be interrupted and should be resumed safely
Suggested setup:
optim = egx.Egor(
xspecs,
failsafe_strategy=egx.FailsafeStrategy.VIABILITY,
)
# First run and subsequent restarts use the same outdir.
# hot_start=0 means resume from latest checkpoint if available.
res = optim.minimize(
fun,
max_iters=60,
outdir="run01",
hot_start=0,
seed=42,
)
Why it helps:
FailsafeStrategy.VIABILITYmodels failure regions and steers search away from themhot_startwith a stableoutdirlets you continue from checkpoints instead of restarting from scratch
Alternatives:
FailsafeStrategy.REJECTION: drops failed points (simplest, and the safest choice when failures are rare)FailsafeStrategy.IMPUTATION: fills failed outputs with surrogate-based estimates. Imputed values are fed back into the surrogates and may drift: check that the imputed objective values stay in a plausible range.
Note:
- Make
funfree of side effects between calls. A simulation that restarts from the state left by the previous call can turn a single failure into a series of failures (every following evaluation starting from a NaN state).
Recipe 9: Restart From an Existing DOE🔗
Use when:
- you already have a DOE from a previous run
- you want to continue optimization without starting from scratch
Suggested setup:
initial_doe = np.load("run01/egor_initial_doe.npy")
optim = egx.Egor(
xspecs,
x_doe=initial_doe,
)
res = optim.minimize(fun, max_iters=40, seed=42)
If the DOE was saved in an output directory from a previous run, you can also let Egor reload it automatically:
optim = egx.Egor(
xspecs
)
res = optim.minimize(
fun,
max_iters=40,
outdir="run01",
warm_start=True,
seed=42,
)
Why it helps:
- reuses already evaluated points instead of recomputing them
- keeps the surrogate and search history aligned with prior work
- makes long optimization runs easier to resume after interruptions
Recipe 10: Constraints Not of the Form ≤ 0🔗
Use when:
- your constraint functions return values that need different feasibility interpretations
- constraints are equality constraints (e.g.,
g(x) = 0) - constraints have lower bounds (e.g.,
g(x) ≥ bound) - constraints have interval bounds (e.g.,
lower ≤ g(x) ≤ upper)
Suggested setup:
import egobox as egx
# Example: constraint must be >= 0 (instead of default <= 0)
optim = egx.Egor(
xspecs,
cstr_specs=[egx.CstrSpec.geq(0.0)], # g(x) >= 0
)
res = optim.minimize(fun, max_iters=40, seed=42)
# Example: equality constraint g(x) = target
optim = egx.Egor(
xspecs,
cstr_specs=[egx.CstrSpec.eq(target_value)],
)
res = optim.minimize(fun, max_iters=40, seed=42)
# Example: constraint in interval [lower, upper]
optim = egx.Egor(
xspecs,
cstr_specs=[egx.CstrSpec.between(lower_bound, upper_bound)],
)
res = optim.minimize(fun, max_iters=40, seed=42)
Why it helps:
cstr_specsdefines the semantics of each constraint beyond the default≤ 0CstrSpec.leq(bound): constraintg(x) ≤ boundCstrSpec.geq(bound): constraintg(x) ≥ boundCstrSpec.eq(value): equality constraintg(x) = valueCstrSpec.between(lower, upper): interval constraintlower ≤ g(x) ≤ upper
Note:
- The constraint function
funshould return raw values;cstr_specsinterprets feasibility - For equality constraints, consider using a small tolerance via
CstrSpec.eq(value, tol=...), or a narrow bandCstrSpec.between(value - eps, value + eps), often easier for the optimizer to satisfy CstrSpec.eqandCstrSpec.betweeneach expand to two internal constraints, both using the spectolEgor(cstr_tol=...)was removed in 1.0.0: give atolto each spec instead- Tolerances are absolute (default
1e-4): scale constraints to order 1 so that the tolerance is meaningful
Recipe 11: Cheap Not Metamodelized Constraint🔗
Use when:
- you have constraints that are cheap to evaluate
- constraints depend only on input variables
x(not on expensive simulations) - you want to avoid the overhead of surrogate modeling for these constraints
Suggested setup:
import egobox as egx
# Example: constraint g(x) <= 1 that is cheap to evaluate (x has shape (nx,))
def cheap_constraint(x):
# Cheap computation based only on x
return x[0] ** 2 + x[1] ** 2 # Circle constraint
def cheap_constraint_grad(x):
return 2.0 * x[:2] # Gradient wrt x (only used with InfillOptimizer.SLSQP)
optim = egx.Egor(
xspecs,
)
res = optim.minimize(
fun,
fcstrs=[(cheap_constraint, cheap_constraint_grad)], # List of cheap constraint functions
fcstr_specs=[egx.CstrSpec.leq(1.0)], # Constraint semantics
max_iters=40,
seed=42,
)
Why it helps:
fcstrs(function constraints) are evaluated directly, not surrogate-modeled- Avoids the complexity of modeling cheap constraints with GPs
- Reduces optimization overhead when constraints are inexpensive
fcstr_specsdefines feasibility interpretation (same ascstr_specs)
Note:
- Use
fcstrsfor cheap constraints; usecstr_specsfor expensive constraints that need surrogate modeling - The
funcallable should return(objective, *constraint_values)when using surrogate constraints, but forfcstrs, constraints are passed separately - Multiple cheap constraints can be provided as a list to
fcstrs - Each constraint is given as
(g, grad_g),{"fun": g, "jac": grad_g}, or a singleg(x, return_grad)callable. Unlike scipy, there is no"type"key: constraints areg(x) <= 0, usefcstr_specsfor other bounds
Recipe 12: Constrained Engineering Problem With Active Constraints🔗
Use when:
- the objective and constraints come from a coupled simulation (e.g. an aero-structural MDA)
- constraints are expressed in physical units (kg, Pa, N, …)
- some constraints are equalities, or are expected to be active at the optimum
- the optimum is likely to lie on variable bounds
Suggested setup:
import numpy as np
import egobox as egx
LW_REF = 100.0 # order of magnitude of the equality constraint (here in kg)
EPS = 1e-3 # accepted band on the scaled equality constraint (here 0.1 kg)
def fun(x):
# obj, stress margins (>= 0, already of order 1), lift minus weight (in kg)
obj, stress, lift_minus_weight = simulate(x)
return np.hstack([obj, stress, lift_minus_weight / LW_REF])
optim = egx.Egor(
xspecs,
cstr_specs=[egx.CstrSpec.geq(0.0)] * 3 + [egx.CstrSpec.between(-EPS, EPS)],
gp_config=egx.GpConfig(corr_spec=egx.CorrelationSpec.MATERN52),
infill_strategy=egx.InfillStrategy.LOG_EI,
infill_optimizer=egx.InfillOptimizer.SLSQP,
cstr_infill=True,
trego=egx.TregoConfig(beta=0.8),
failsafe_strategy=egx.FailsafeStrategy.REJECTION,
)
res = optim.minimize(fun, max_iters=200, outdir="run_s42", seed=42, timeout=3200.0)
Why it helps:
- The feasibility tolerance (spec
tol) is absolute (default1e-4on every internal constraint): scaling each constraint to order 1 makes it meaningful. A constraint in kg with values around 1000 is almost never considered satisfied. - A narrow band
CstrSpec.between(-eps, eps)is much easier to satisfy thanCstrSpec.eq(0.0)while keeping the equality precise enough in practice. - The
SLSQPinfill optimizer follows active constraint boundaries more accurately thanCOBYLA. cstr_infill=Trueweights the infill criterion by the probability of feasibility, which drives the search towards the feasible region when the initial DoE contains no feasible point.REJECTIONis the safest failsafe strategy when simulation failures are rare. In this case study,VIABILITY(modeling the failure region) did not reduce the number of failures, made iterations more expensive and gave a similar or worse objective.timeoutstops the optimization gracefully before a batch-job time limit.
Case study (11 variables, 3 stress constraints, 1 lift = weight equality, 200 iterations, seed 42):
| Setting | Feasible points | Best feasible objective (reference 41.84) |
|---|---|---|
| unscaled equality constraint (in kg) | 0 / 124 | none |
scaled, CstrSpec.eq, COBYLA | 5 / 186 | 37.21 |
scaled, CstrSpec.between band, COBYLA | 8 / 206 | 41.24 |
as above with WB2S infill criterion | 8 / 207 | 32.25 |
scaled, CstrSpec.between band, SLSQP | 13 / 210 | 41.81 |
Across seeds, the last setup reached within 0.5% of the gradient-based reference in about
60 to 140 iterations, but not always within 0.1%. With max_iters=200, it ended within 0.07%
of the reference on the three seeds tested, for a run time of 1.1 to 1.7 times that of the
gradient-based optimizer (which needs the derivatives of the simulation).
Failure handling on the same problem (200 iterations):
| Seed | REJECTION: objective | failed points | VIABILITY: objective | failed points |
|---|---|---|---|---|
| 42 | 41.824 | 7 | 41.827 | 15 |
| 1 | 41.828 | 7 | 41.811 | 3 |
| 3 | 41.807 | 34 | 41.700 | 32 |
Note:
- Make
funfree of side effects between calls: if the simulation restarts from the state of a previous call, one diverged (NaN) evaluation can make all the following ones fail. - Compare configurations with at least two seeds: in this case study the seed alone changed the final objective by up to 1.3%.