Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 92 additions & 5 deletions examples/alphaevolve_interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ check. These helpers do not replace the cloud SDK or implement all of #468.
The first SDK-entrypoint slice is explicitly namespaced under
`openevolve.compat.alpha_evolve`. It provides a small, synchronous lifecycle
backed by OpenEvolve's public `run_evolution` API, without installing Google's
SDK or reading Google credentials. It does **not** register a top-level
`alpha_evolve` alias or change imports in other code.
SDK or reading Google credentials. Importing the facade does **not** register a
top-level `alpha_evolve` alias or change imports in other code. Scoped aliases
require the explicit opt-in described below.

This self-contained example runs only a seed by default:

Expand Down Expand Up @@ -109,15 +110,101 @@ failure insight. `run_controller_loop` only checks successful completion and
accepts no cloud worker or timeout options.

This is **not yet** enough to execute the original three upstream entrypoints
unchanged: SDK model types, visualization helpers, import aliases, complete
archive access and remote client/controller methods remain outside this slice.
The earlier runner below remains available for unchanged upstream evaluators.
unchanged: complete archive access, remote client/controller methods and the
full SDK model surface remain unsupported. Local connection settings are still
required, and cloud-specific configuration/controller calls need adaptation.
The runner below remains available for unchanged upstream evaluators.
No managed Google Cloud run is implied by a local facade run.

```bash
python -m unittest discover -s tests -p 'test_alphaevolve_local_experiment.py' -v
```

## Load evaluator imports without Google's SDK

Five dependency-free evaluation envelopes now cover the local examples'
`alpha_evolve.models` imports: `AlphaEvolveEvaluationScore`,
`AlphaEvolveEvaluationScores`, `AlphaEvolveEvaluationInsight`,
`AlphaEvolveEvaluationInsights` and `AlphaEvolveProgramEvaluation`. They live in
`openevolve.compat.alpha_evolve.models`. These are dataclasses with attribute
access and a no-argument `model_dump()` returning independent nested dictionaries,
including optional `None` fields. They do **not** implement Pydantic validation,
coercion, schemas, `model_validate` or serialization options. The existing
evaluator adapter continues to validate metrics and map failures.

Load an unchanged, trusted upstream evaluator with the local types explicitly:

```python
from pathlib import Path
from openevolve.compat.alpha_evolve import load_evaluator
from openevolve.compat.alphaevolve import adapt_evaluator

example = Path("../alphaevolve-reference/examples/circle_packing")
evaluate = load_evaluator(example / "src/evaluate.py", "circle_packing_evaluation")
adapted = adapt_evaluator(evaluate, objective_metric="sum_of_radii")
print(adapted(example / "src/program.py").metrics)
# Or pass evaluate to the local AlphaEvolveExperiment from the previous section.
```

The helper reads and executes the selected `.py` file, including its import-time
code, under temporary local aliases. It returns the raw candidate-based callable
and removes both the aliases and temporary file-module registration. Registration
during import supports decorators such as `@dataclass`; removal afterwards lets
cloudpickle transport the evaluator by value to fresh OE workers, whose local
envelope classes remain importable under the `openevolve` namespace. No SDK alias
needs to remain active during evolution. The helper uses the current source
bytes rather than timestamp/size-based cached bytecode.

Evaluator dependencies such as NumPy, SciPy and Matplotlib must still be
installed. Relative/sibling imports are not managed. Import-time side effects
outside the temporary module entries are not undone, and candidate/evaluator
code is trusted with the current process's privileges. This adds no sandbox.

For explicit import migration, `local_imports` exposes temporary `client`,
`experiment`, `controller`, `models` and `visualization` modules:

```python
from openevolve.compat.alpha_evolve import local_imports

with local_imports():
from alpha_evolve.client import AlphaEvolveClient
from alpha_evolve.visualization import get_score
# Existing references still work; sys.modules aliases have been removed.
```

This context is process-global, main-thread-only and intended for a
single-threaded entrypoint, not a concurrent service. It refuses to enter if
**any** `alpha_evolve` namespace is already loaded, even a nested local context;
use a fresh process rather than shadowing a real SDK. An installed but unimported
SDK is temporarily bypassed only inside the explicit context. Unknown SDK
submodules do not fall back to the cloud package. Exit, including on exception
or interruption, removes only module entries still owned by the context; it
never overwrites replacements made by other code. It does not install packages,
change import paths or credentials, or construct clients. The aliased local
client still requires explicit OE endpoint/objective settings. For evaluator
files, prefer `load_evaluator` so worker serialization does not depend on aliases.

`openevolve.compat.alpha_evolve.visualization.get_score(program, metric)` reads
mapping or attribute-based result envelopes without plotting or executing code.
It preserves finite scores (including zero and negatives), returning `-inf` for
absent, malformed, boolean or non-finite scores.

The existing CLI also accepts `--local-model-types` as a scoped opt-in, including
with seed-only evaluation or imported/OE evolution configs. Without that flag,
its existing Google model imports remain unchanged:

```bash
# Requires the example's scientific dependencies, but no Google's SDK source path.
MPLBACKEND=Agg python examples/alphaevolve_interop/run.py \
../alphaevolve-reference/examples/circle_packing --local-model-types

python -m unittest discover -s tests -p 'test_alphaevolve_local_imports.py' -v
```

This closes the local evaluator-type/import gap, not the complete SDK shim or
the original-entrypoint acceptance criterion for #468. It neither enables cloud
execution nor changes the facade's budget, concurrency or best-only query limits.

## Evaluate an upstream seed locally

From an installed OpenEvolve checkout:
Expand Down
14 changes: 12 additions & 2 deletions examples/alphaevolve_interop/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
}


def load_evaluator(example_dir):
def load_evaluator(example_dir, *, local_model_types=False):
"""Load the example without importing its cloud client or experiment entrypoint."""
function_name, objective = EXAMPLES[example_dir.name]
if local_model_types:
from openevolve.compat.alpha_evolve import load_evaluator as load_local_evaluator

evaluator = load_local_evaluator(example_dir / "src" / "evaluate.py", function_name)
return adapt_evaluator(evaluator, objective_metric=objective)
spec = importlib.util.spec_from_file_location(
"_alphaevolve_example", example_dir / "src" / "evaluate.py"
)
Expand All @@ -34,6 +39,11 @@ def load_evaluator(example_dir):
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("example_dir", type=Path, help="Path to the upstream example directory")
parser.add_argument(
"--local-model-types",
action="store_true",
help="Explicitly load evaluator imports with local envelopes instead of Google's SDK",
)
sources = parser.add_mutually_exclusive_group()
sources.add_argument(
"--config", type=Path, help="OE YAML config; starts evolution (legacy mode)"
Expand Down Expand Up @@ -115,7 +125,7 @@ def main(argv=None):
for path in (program_path, example_dir / "src" / "evaluate.py"):
if not path.is_file():
parser.error(f"Required example file not found: {path}")
evaluator = load_evaluator(example_dir)
evaluator = load_evaluator(example_dir, local_model_types=args.local_model_types)
if not evolving:
result = evaluator(program_path)
print(json.dumps({"metrics": result.metrics, "artifacts": result.artifacts}, indent=2))
Expand Down
11 changes: 9 additions & 2 deletions openevolve/compat/alpha_evolve/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
"""Explicit local experiment facade; does not install an ``alpha_evolve`` alias."""
"""Local experiment facade; import aliases require an explicit scoped opt-in."""

from .client import AlphaEvolveClient
from .controller import run_controller_loop
from .experiment import AlphaEvolveExperiment
from .imports import load_evaluator, local_imports

__all__ = ["AlphaEvolveClient", "AlphaEvolveExperiment", "run_controller_loop"]
__all__ = [
"AlphaEvolveClient",
"AlphaEvolveExperiment",
"run_controller_loop",
"load_evaluator",
"local_imports",
]
83 changes: 83 additions & 0 deletions openevolve/compat/alpha_evolve/imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Explicit, temporary import aliases and by-value evaluator loading."""

import importlib
import importlib.util
import sys
import threading
import uuid
from contextlib import contextmanager
from importlib.machinery import ModuleSpec
from pathlib import Path
from types import ModuleType
from typing import Callable, Iterator, Union


@contextmanager
def local_imports() -> Iterator[None]:
"""Temporarily route ``alpha_evolve`` imports to the local facade.

This opt-in context is process-global and main-thread-only, not thread
isolation. Use it in a single-threaded entrypoint, not a concurrent service.
Refuse any pre-existing SDK namespace (including nested local contexts).
Never change installed packages, import paths, files, or credentials.
Only this context's own module entries are removed on exit.
"""
if threading.current_thread() is not threading.main_thread():
raise RuntimeError("Use local_imports on the main thread in a single-threaded entrypoint")
if any(name == "alpha_evolve" or name.startswith("alpha_evolve.") for name in sys.modules):
raise RuntimeError("alpha_evolve is already loaded; use a fresh process for local imports")

modules = {
name: importlib.import_module(f"openevolve.compat.alpha_evolve.{name}")
for name in ("client", "controller", "experiment", "models", "visualization")
}
package = ModuleType("alpha_evolve", "Explicit, temporary OpenEvolve local facade")
package.__package__ = "alpha_evolve"
package.__path__ = []
package.__spec__ = ModuleSpec("alpha_evolve", loader=None, is_package=True)
for name, module in modules.items():
setattr(package, name, module)
aliases = {"alpha_evolve": package}
aliases.update({f"alpha_evolve.{name}": module for name, module in modules.items()})
sys.modules.update(aliases)
try:
yield
finally:
for name, module in aliases.items():
if sys.modules.get(name) is module:
del sys.modules[name]


def load_evaluator(path: Union[str, Path], function_name: str) -> Callable:
"""Import a trusted Python evaluator with local model types, then remove aliases.

Return the raw candidate-based callable; adapt it with ``adapt_evaluator``
or pass it to the local ``AlphaEvolveExperiment``. The file module is removed
after import so cloudpickle transports the callable by value to OE workers.
Relative/sibling imports and arbitrary import side effects are not managed.
"""
path = Path(path).resolve()
if path.suffix != ".py" or not path.is_file():
raise ValueError("path must name an existing Python .py evaluator file")
if not isinstance(function_name, str) or not function_name.isidentifier():
raise ValueError("function_name must be a Python identifier")
name = "_openevolve_local_ae_evaluator_" + uuid.uuid4().hex
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise ImportError("Cannot load the evaluator file")
module = importlib.util.module_from_spec(spec)
with local_imports():
# Registration during import supports decorators such as @dataclass.
# Remove it afterwards to select cloudpickle's by-value transport.
sys.modules[name] = module
try:
# Read the requested source, not timestamp/size-based cached bytecode:
# callers may have just edited a same-size evaluator in this second.
exec(compile(path.read_bytes(), str(path), "exec"), module.__dict__)
evaluator = getattr(module, function_name, None)
if not callable(evaluator):
raise ValueError("function_name must select a callable in the evaluator file")
finally:
if sys.modules.get(name) is module:
del sys.modules[name]
return evaluator
52 changes: 52 additions & 0 deletions openevolve/compat/alpha_evolve/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Dependency-free evaluation envelopes used by local AlphaEvolve examples.

These dataclasses support attribute access and a no-argument ``model_dump``.
They do not emulate Pydantic validation, coercion, schemas or serialization
options. The evaluator adapter validates scores before OpenEvolve uses them.
"""

from dataclasses import asdict, dataclass
from typing import Any, Optional


class _Envelope:
def model_dump(self) -> dict[str, Any]:
"""Return an independent nested dictionary, including optional None fields."""
return asdict(self)


@dataclass
class AlphaEvolveEvaluationScore(_Envelope):
metric: str
score: Optional[float] = None


@dataclass
class AlphaEvolveEvaluationScores(_Envelope):
scores: list[AlphaEvolveEvaluationScore]


@dataclass
class AlphaEvolveEvaluationInsight(_Envelope):
label: str
text: str


@dataclass
class AlphaEvolveEvaluationInsights(_Envelope):
insights: list[AlphaEvolveEvaluationInsight]


@dataclass
class AlphaEvolveProgramEvaluation(_Envelope):
scores: AlphaEvolveEvaluationScores
insights: Optional[AlphaEvolveEvaluationInsights] = None


__all__ = [
"AlphaEvolveEvaluationScore",
"AlphaEvolveEvaluationScores",
"AlphaEvolveEvaluationInsight",
"AlphaEvolveEvaluationInsights",
"AlphaEvolveProgramEvaluation",
]
31 changes: 31 additions & 0 deletions openevolve/compat/alpha_evolve/visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Score lookup for local result envelopes; no plotting dependencies."""

import math
from typing import Any

from openevolve.compat.alphaevolve import _field


def get_score(program: Any, metric_name: str) -> float:
"""Read a finite named score, or -inf for absent/invalid values.

Accepts mappings or attribute-based envelopes. Unlike a plotting helper,
this does not execute a candidate or import visualization libraries.
"""
scores = _field(_field(_field(program, "evaluation"), "scores"), "scores")
if not isinstance(scores, (list, tuple)):
return -math.inf
for entry in scores:
if _field(entry, "metric") != metric_name:
continue
value = _field(entry, "score")
if value is None:
continue
if isinstance(value, bool):
return -math.inf
try:
score = float(value)
except (TypeError, ValueError, OverflowError):
return -math.inf
return score if math.isfinite(score) else -math.inf
return -math.inf
Loading
Loading