Ensemble-Based Optimization
The Quickstart minimized a single, fixed objective. Here we work through an uncertain problem: the objective depends on parameters we do not know exactly. We now have a set of functions, each with different parameters drawn from some (possibly unknown) probability distribution. Each member of the set is a realization. The full runnable script is examples/simple/ensemble.py.
ropt optimizes the realizations together by combining them into a single
robust objective — by default a weighted average over the realizations.
Minimizing the average yields a solution that performs well across the whole set
rather than for one particular case. We minimize the Rosenbrock function,
generalized to \(n\) variables, with coefficients that vary between realizations.
For realization \(i\), with coefficients \(a_i\) and \(b_i\), the per-realization objective is:
which it reduces to the standard Rosenbrock function when \(n = 1\), and \(a_i = 1, b_i = 100\) for every realization.
ropt combines the realizations into the robust objective:
with weights \(w_i\) that we set in the configuration below.
1. Describe the problem
The config adds a realizations section: the weights list has one entry per
realization that sets how much each contributes to the combined objective:
DIM = 5
REALIZATIONS = 10
UNCERTAINTY = 0.1
CONFIG: dict[str, Any] = {
"variables": {
"variable_count": DIM,
"perturbation_magnitudes": 1e-6,
},
"realizations": {
"weights": [1.0] * REALIZATIONS,
},
}
INITIAL_VALUES = 2 * np.arange(DIM) / DIM + 0.5
The weights need not sum to one; ropt normalizes them. Equal weights, as here,
give a plain average. See Configuration for the
other realization settings. INITIAL_VALUES is the point the optimization
starts from.
2. Draw the uncertain parameters
Each realization is one draw of the uncertain parameters. Here the two Rosenbrock
coefficients are sampled once per realization, so that a[r] and b[r] are the
coefficients for realization r:
rng = default_rng(seed=123)
a = rng.normal(loc=1.0, scale=UNCERTAINTY, size=REALIZATIONS)
b = rng.normal(loc=100.0, scale=100 * UNCERTAINTY, size=REALIZATIONS)
3. Write the evaluation function
ropt calls the evaluation function once for every realization at each point it
evaluates, so it must return the value for its own realization. The second
argument tells it which one: context.realization is the realization number,
which we use to index the parameter arrays:
def rosenbrock(
variables: NDArray[np.float64], context: EvaluationFunctionContext
) -> float:
"""The Rosenbrock function of one realization, with uncertain coefficients.
Args:
variables: The variable vector to evaluate.
context: Identifies the realization being evaluated.
Returns:
The objective of this realization at `variables`.
"""
r = context.realization
objective = 0.0
for d_idx in range(DIM - 1):
x, y = variables[d_idx : d_idx + 2]
objective += (a[r] - x) ** 2 + b[r] * (y - x * x) ** 2
return float(objective)
The Quickstart ignored this second argument; an ensemble
objective uses context.realization to select the parameters for the
realization it is computing. ropt combines the per-realization values into the
robust objective for you.
Returning a single number, as here, is the simplest case. A function that has multiple objectives and has constraints returns a sequence instead — the objectives first, then the constraints; see Constraints.
4. Follow the progress (optional)
To track a running optimization, pass a report callback: ropt calls it after
every evaluation, with an EvaluateResult
describing what was just computed.
def report(result: EvaluateResult) -> None:
"""Print the objective of each evaluation as the run proceeds.
Args:
result: The result of a single function evaluation.
"""
if result.target_objective is not None:
print(f" objective: {result.target_objective}")
The callback belongs to one run and sees one evaluation at a time. To keep the results rather than just look at them — or to collect them across several runs — use a handler instead; see Collecting Results with Handlers.
5. Run it
The call is the same as for a deterministic problem, with INITIAL_VALUES the
start point defined above:
ropt evaluates all ten realizations at each point and averages them into the
robust objective, which is what it optimizes.
6. Read the result
optimize returns an OptimizeResult:
print(f"exit code: {result.exit_code}")
print(f"optimal variables: {result.variables}")
print(f"optimal objective: {result.target_objective}")
result.variablesis the best set of variables found, andresult.target_objectivethe robust objective value there. Both areNoneif the run produced no valid result.result.exit_codesays why the run stopped (a member of theExitCodeenumeration).result.resultsholds the full low-level result (aFunctionResultsobject), if you need every detail.
See The result for the remaining fields.
Because the coefficients are centered on the values used in the Quickstart, the robust optimum still lies close to where all variables equal 1 — but it minimizes the average over the uncertain coefficients rather than any single realization.
Where to next
- Collect every result, not just the best one: Collecting Results with Handlers.
- The complete simple API: Running Optimizations.
- All realization settings: Configuration.
- The ideas and terms behind ensembles: Key Concepts.