OptimizationStrategy#

Layer 3: Extend Components

saealib delegates the selection of candidates sent for expensive true evaluation to OptimizationStrategy. The Strategy describes candidate generation, prediction, evaluation planning, Feedback, and Population updates as one graph.

OptimizationStrategy’s role#

The graph-native extension point is build_graph(provider) -> ComponentGraph. Before execution, Optimizer passes this graph to the Compiler to create an ExecutablePlan.

build_pipeline(provider) -> Pipeline is the Stage compatibility path and an optional compatibility representation for Strategies that use the existing Pipeline DSL. Even in this form, a Stage inserted into a structured Pipeline must be wrapped explicitly with stage_component(stage).

step(ctx, provider) is the remaining boundary on the Stage compatibility path. A new graph-native component reads a StateView and returns a StatePatch or NodeResult.

The class attribute requires_surrogate: bool indicates whether this strategy needs a SurrogateManager. Optimizer.validate() checks this attribute to confirm you aren’t trying to use a strategy with requires_surrogate=True while surrogate_manager is unset.

Built-in Strategies#

Class

Parameters

Approach

DirectStrategy

None

Uses no surrogate; truly evaluates every generated candidate

SteadyStateStrategy

None

Generates one candidate per step and supports asynchronous refill

IndividualBasedStrategy

evaluation_ratio: float = 0.1

Scores every candidate with the surrogate, and truly evaluates only the top evaluation_ratio fraction

PreSelectionStrategy

n_candidates: int, n_select: int (both required)

Generates n_candidates, scores them, and truly evaluates only the top n_select

GenerationBasedStrategy

gen_ctrl: int (required)

Advances gen_ctrl generations using only the surrogate, then truly evaluates a single generation

IndividualBasedStrategy selects by fraction of individuals, while PreSelectionStrategy selects by count of individuals. GenerationBasedStrategy switches between surrogate and true evaluation by generation, not by individual. DirectStrategy is the comparison baseline that uses no surrogate at all, with requires_surrogate=False.

For steady-state execution, combine DirectStrategy with an AsyncEvaluationScheduler. The scheduler fills available worker slots, polls without blocking, and commits each completed update in the lifecycle order.

Each Strategy’s pipeline structure#

Class

Pipeline

DirectStrategy

CountGeneration → Ask → TrueEvaluation → ArchiveUpdate → Tell

IndividualBasedStrategy

CountGeneration → Ask → SurrogatePredictStage → AcquisitionStage → SortByScore → TrueEvaluation (ratio-based) → ArchiveUpdate → Tell

PreSelectionStrategy

CountGeneration → Ask (n_candidates) → SurrogatePredictStage → AcquisitionStage → TopKSelection(k=n_select) → TrueEvaluation → ArchiveUpdate → Tell

GenerationBasedStrategy

SurrogateOnlyLoop (gen_ctrl times) → CountGeneration → Ask → TrueEvaluation → ArchiveUpdate → Tell

Each Strategy’s components include the EvaluationPlan, asynchronous evaluation submission and collection, Feedback, and Tell. A Stage is a compatibility execution unit for part of that work, not the same execution contract as the entire structured Pipeline. See Stage and Framework for details.

Which Strategy to choose#

For problems where evaluation cost is extremely high, IndividualBasedStrategy or PreSelectionStrategy, which use the surrogate to filter out most candidates, are effective. Early in the search, when the surrogate’s reliability is still low, or when you don’t want to pay the surrogate’s own training cost frequently, GenerationBasedStrategy, which advances multiple generations at once using only the surrogate, is suited. For problems where the surrogate’s approximation error itself is unacceptable, or evaluation cost is sufficiently low, relying solely on true evaluation via DirectStrategy is reasonable.

Behavior of runtime swapping#

When you change configuration such as provider.algorithm or provider.surrogate_manager at a step boundary in iterate() or run(), the execution environment detects the change, recompiles the plan, and applies it from the next generation. This procedure works on both the Stage compatibility path and the graph-native path. When a Component requests recompilation, the plan node returns RuntimeCommand or RequestRecompile through NodeResult.commands. The request is accepted when the Runtime environment provides recompile(), and recompilation happens between steps. Rejection is also a normal result.

Implementing a custom Strategy#

When implementing a custom candidate-selection method, choose separately between the graph-native path and the Stage compatibility path.

Graph-native path: Implement build_graph() and return a ComponentGraph. For a gradual migration using the Pipeline DSL, wrap each Stage with stage_component() before passing it to lower_pipeline().

from saealib import OptimizationStrategy, Pipeline
from saealib.core import lower_pipeline
from saealib.stages import stage_component


class CustomStrategy(OptimizationStrategy):
    """Skeleton for building a Strategy ComponentGraph from the Pipeline DSL."""

    requires_surrogate = False

    def build_graph(self, provider):
        pipeline = Pipeline(
            steps=[
                stage_component(first_stage),
                stage_component(second_stage),
            ]
        )
        return lower_pipeline(pipeline)

first_stage and second_stage stand for the Stage instances to combine. This example shows only the boundary for building a ComponentGraph from a Pipeline; add evaluation-plan and Feedback contracts according to the Stage composition.

When adjusting only part of an existing Strategy, changing the compatibility Pipeline with Pipeline.replace/find is preferable to creating a new Strategy.

References#