Source code for saealib.strategies.base

"""Base class for optimization strategies."""

from __future__ import annotations

from saealib.context import OptimizationState
from saealib.core.compiler.graph import ComponentGraph
from saealib.core.contracts import ComponentContract, PortContract, StateContract
from saealib.core.state import PENDING_EVALUATIONS
from saealib.optimizer import ComponentProvider
from saealib.pipeline import Pipeline
from saealib.policies.evaluation import EvaluateAll, EvaluationPlanner
from saealib.policies.feedback import FeedbackBuilder, MixedFeedback


def build_runtime_neutral_graph(
    strategy: OptimizationStrategy, provider: ComponentProvider
) -> ComponentGraph:
    """Build the strategy's one canonical graph topology.

    Runtime selection changes execution policy, never graph construction.
    Built-in strategies materialize their graph directly at this boundary;
    ``build_pipeline`` is reconstructed from this graph only for compatibility
    callers.
    """
    if type(strategy).build_graph is not OptimizationStrategy.build_graph:
        return strategy.build_graph(provider)
    raise TypeError("strategy must implement build_graph")


def build_pipeline_from_graph(graph: ComponentGraph) -> Pipeline:
    """Recover a Pipeline facade from a stage-based strategy graph."""
    from saealib.core.graph_builder import StageNodeAdapter

    stages = [
        node.component.stage
        for node in graph.nodes
        if isinstance(node.component, StageNodeAdapter)
    ]
    return Pipeline(stages)


[docs] class OptimizationStrategy: """Base class for optimization strategies. Strategies expose one canonical :class:`ComponentGraph` topology. A strategy may describe it directly through ``build_pipeline``; the default implementation recovers a facade for stage-based graphs. """ # Optimizer.validate() checks this to ensure surrogate_manager is configured. requires_surrogate: bool = False # Async runtimes may refill a strategy while an earlier proposal is still # waiting for complete feedback. Most strategies need a generation # boundary first; steady-state strategies explicitly opt into overlap. supports_async_refill: bool = False evaluation_planner: EvaluationPlanner = EvaluateAll() feedback_builder: FeedbackBuilder = MixedFeedback()
[docs] def contract(self) -> ComponentContract: """Return the strategy contract.""" return ComponentContract( ports={"strategy": PortContract()}, state=StateContract( reads=(PENDING_EVALUATIONS,), ), )
[docs] def build_pipeline(self, provider: ComponentProvider) -> Pipeline: """Return the strategy's high-level Pipeline description.""" return build_pipeline_from_graph(self.build_graph(provider))
[docs] def build_graph(self, provider: ComponentProvider) -> ComponentGraph: """Build the strategy's canonical graph. Built-in strategies provide node/adapter specs to the canonical graph builder; graph-only extensions override this method and need not implement a Pipeline builder. """ return build_runtime_neutral_graph(self, provider)
[docs] def step( self, ctx: OptimizationState, provider: ComponentProvider ) -> OptimizationState: """ Perform one generation step: generate, score, evaluate, and update. Parameters ---------- ctx : OptimizationState Current optimization context. provider : ComponentProvider Component provider. Returns ------- OptimizationState Updated state returned by the pipeline. """ from saealib.execution.runtime import execute_strategy_step return execute_strategy_step(self, ctx, provider)