OpenEvolve
Island-model MAP-Elites evolutionary search with diff mutation (the open AlphaEvolve).
"""OpenEvolve — a faithful port of the real OpenEvolve (the open AlphaEvolve).
Every component is the OpenEvolve-specific subclass from :mod:`.components`:
* Population → :class:`MapElitesIslandsPopulation` — per-island MAP-Elites grids over
``feature_dimensions`` + global elite archive + lazy ring migration (``ProgramDatabase``).
* Selection → :class:`OpenEvolveSelectionPolicy` — round-robin island rotation + 3-tier
explore/exploit/random parent sampling + island-best/elite/diverse/random inspirations.
* PromptBuilder → :class:`OpenEvolvePromptBuilder` — the OpenEvolve diff template, section for section.
* Proposer → :class:`~galapagos.components.proposer.DiffProposer` — SEARCH/REPLACE + full-rewrite
parsing, matching OpenEvolve's ``apply_diff`` / ``parse_full_rewrite``.
The scaffold adds OpenEvolve's per-iteration island bookkeeping: after each admitted child it
increments that island's generation counter and, when ``max(generations) - last_migration >=
migration_interval``, performs ring migration (``process_parallel.py`` island-management block).
"""
from __future__ import annotations
import logging
from ...config import GalapagosConfig
from ...models import GalapagosModel
from ...records import Genome
from ..base_scaffold import GalapagosScaffold
from ..registry import register_scaffold
# one module per component (the OpenEvolve scaffold method)
from .memory import OpenEvolveMemory
from .population import MapElitesIslandsPopulation
from .prompt_builder import OpenEvolvePromptBuilder
from .proposer import OpenEvolveProposer
from .selection_policy import OpenEvolveSelectionPolicy
log = logging.getLogger(__name__)
@register_scaffold("openevolve")
class OpenEvolveScaffold(GalapagosScaffold):
name = "openevolve"
@classmethod
def build_components(cls, config: GalapagosConfig, model: GalapagosModel | None) -> dict:
seed = int(config.seed)
pop = config.population
sel = config.selection_policy
num_islands = int(pop.num_islands)
feature_dimensions = list(pop.feature_dimensions)
return {
"population": MapElitesIslandsPopulation(
num_islands=num_islands,
archive_size=int(pop.archive_size),
population_size=int(pop.population_size),
feature_dimensions=feature_dimensions,
feature_bins=int(pop.feature_bins),
migration_interval=int(pop.migration_interval),
migration_rate=float(pop.migration_rate),
diversity_reference_size=int(pop.diversity_reference_size),
),
"selection_policy": OpenEvolveSelectionPolicy(
seed=seed,
num_islands=num_islands,
exploration_ratio=float(sel.exploration_ratio),
exploitation_ratio=float(sel.exploitation_ratio),
elite_selection_ratio=float(sel.elite_selection_ratio),
num_inspirations=int(sel.num_inspirations),
num_diverse=int(sel.num_diverse),
feature_dimensions=feature_dimensions,
),
"prompt_builder": OpenEvolvePromptBuilder(
num_top_programs=int(sel.num_inspirations),
num_diverse=int(sel.num_diverse),
feature_dimensions=feature_dimensions,
),
"proposer": OpenEvolveProposer(),
"memory": OpenEvolveMemory(),
}
def setup(self, task) -> None:
super().setup(task)
# OpenEvolve's EvaluatorConfig defaults to a 3-stage cascade with thresholds [0.5, 0.75, 0.9]
# (vs the framework/SkyDiscover default [0.3, 0.6]). Apply OpenEvolve's gates unless the task
# card declared its own — the task knows its stage semantics best. Only bites cascade tasks.
from ...components.evaluator import ContainerEvaluator, SubprocessEvaluator
# Both deployments (local subprocess + Docker sandbox) carry cascade_thresholds/max_retries,
# so OpenEvolve's gates apply identically whether the task runs locally or in a container.
if isinstance(self.evaluator, (SubprocessEvaluator, ContainerEvaluator)):
card_eval = (getattr(getattr(self.task, "card", None), "evaluation", None) or {})
if card_eval.get("cascade_thresholds") is None:
self.evaluator.cascade_thresholds = [0.5, 0.75, 0.9]
# OpenEvolve EvaluatorConfig.max_retries default = 3 (re-run a candidate on transient failure).
self.evaluator.max_retries = int(card_eval.get("max_retries", 3))
# ---- OpenEvolve island bookkeeping (process_parallel.py island-management block) ---------
def after_step(self, child: Genome, result) -> None:
"""After an admitted child: bump its island's generation counter, then migrate if due."""
if result is None: # NO_DIFF step (no child evaluated/added) — nothing to bump
return
pop = self.population
if not isinstance(pop, MapElitesIslandsPopulation):
return
island = child.metadata.get("island", pop.current_island)
pop.increment_island_generation(island)
if pop.should_migrate():
pop.migrate()
log.debug("ring migration — island=%s iter=%d", island, self.state.iteration)