@@ -141,6 +141,13 @@ async def _run_evolution_async(
141141 # Process evaluator
142142 evaluator_path = _prepare_evaluator (evaluator , temp_dir , temp_files )
143143
144+ # Auto-disable cascade evaluation if the evaluator doesn't define stage functions
145+ if config_obj .evaluator .cascade_evaluation :
146+ with open (evaluator_path , "r" ) as f :
147+ eval_content = f .read ()
148+ if "evaluate_stage1" not in eval_content :
149+ config_obj .evaluator .cascade_evaluation = False
150+
144151 # Create and run controller
145152 controller = OpenEvolve (
146153 initial_program_path = program_path ,
@@ -239,13 +246,40 @@ def _prepare_evaluator(
239246
240247 # If it's a callable, create a wrapper module
241248 if callable (evaluator ):
242- # Create a unique global name for this evaluator
243- evaluator_id = f"_openevolve_evaluator_{ uuid .uuid4 ().hex [:8 ]} "
249+ # Try to get the source code of the callable so it can be serialized
250+ # into a standalone file that works in subprocesses
251+ try :
252+ func_source = inspect .getsource (evaluator )
253+ # Dedent in case the function was defined inside another scope
254+ import textwrap
255+
256+ func_source = textwrap .dedent (func_source )
257+ func_name = evaluator .__name__
258+
259+ # Build a self-contained evaluator module with the function source
260+ # and an evaluate() entry point that calls it
261+ evaluator_code = f"""
262+ # Auto-generated evaluator from user-provided callable
263+ import importlib.util
264+ import sys
265+ import os
266+ import copy
267+ import json
268+ import time
269+
270+ { func_source }
244271
245- # Store in globals so the wrapper can find it
246- globals ()[evaluator_id ] = evaluator
272+ def evaluate(program_path):
273+ '''Wrapper that calls the user-provided evaluator function'''
274+ return { func_name } (program_path)
275+ """
276+ except (OSError , TypeError ):
277+ # If we can't get source (e.g. built-in, lambda, or closure),
278+ # fall back to the globals-based approach
279+ evaluator_id = f"_openevolve_evaluator_{ uuid .uuid4 ().hex [:8 ]} "
280+ globals ()[evaluator_id ] = evaluator
247281
248- evaluator_code = f"""
282+ evaluator_code = f"""
249283# Wrapper for user-provided evaluator function
250284import { __name__ } as api_module
251285
@@ -335,57 +369,67 @@ def initial_sort(arr):
335369 lines .insert (func_end + 1 , " " * (indent + 4 ) + "# EVOLVE-BLOCK-END" )
336370 func_source = "\n " .join (lines )
337371
338- # Create evaluator that tests the function
339- def evaluator (program_path ):
340- import importlib .util
341- import sys
372+ # Create a self-contained evaluator as a code string so it works in subprocesses.
373+ # Closure-based evaluators fail with process-based parallelism because subprocess
374+ # workers cannot access the parent process's memory.
375+ evaluator_code = f"""
376+ import importlib.util
377+ import copy
342378
343- # Load the evolved program
344- spec = importlib .util .spec_from_file_location ("evolved" , program_path )
345- if spec is None or spec .loader is None :
346- return {"score" : 0.0 , "error" : "Failed to load program" }
379+ FUNC_NAME = { func_name !r}
380+ TEST_CASES = { test_cases !r}
347381
348- module = importlib .util .module_from_spec (spec )
382+ def evaluate(program_path):
383+ '''Auto-generated evaluator for evolve_function'''
384+ # Load the evolved program
385+ spec = importlib.util.spec_from_file_location("evolved", program_path)
386+ if spec is None or spec.loader is None:
387+ return {{"combined_score": 0.0, "score": 0.0, "error": "Failed to load program"}}
349388
350- try :
351- spec .loader .exec_module (module )
352- except Exception as e :
353- return {"score" : 0.0 , "error" : f"Failed to execute program: { str (e )} " }
389+ module = importlib.util.module_from_spec(spec)
354390
355- if not hasattr (module , func_name ):
356- return {"score" : 0.0 , "error" : f"Function '{ func_name } ' not found" }
391+ try:
392+ spec.loader.exec_module(module)
393+ except Exception as e:
394+ return {{"combined_score": 0.0, "score": 0.0, "error": f"Failed to execute program: {{str(e)}}"}}
357395
358- evolved_func = getattr (module , func_name )
359- correct = 0
360- total = len (test_cases )
361- errors = []
396+ if not hasattr(module, FUNC_NAME):
397+ return {{"combined_score": 0.0, "score": 0.0, "error": f"Function '{{FUNC_NAME}}' not found"}}
362398
363- for input_val , expected in test_cases :
364- try :
365- # Handle case where input is a list/mutable - make a copy
366- if isinstance (input_val , list ):
367- test_input = input_val .copy ()
368- else :
369- test_input = input_val
370-
371- result = evolved_func (test_input )
372- if result == expected :
373- correct += 1
374- else :
375- errors .append (f"Input { input_val } : expected { expected } , got { result } " )
376- except Exception as e :
377- errors .append (f"Input { input_val } : { str (e )} " )
378-
379- return {
380- "score" : correct / total ,
381- "test_pass_rate" : correct / total ,
382- "tests_passed" : correct ,
383- "total_tests" : total ,
384- "errors" : errors [:3 ], # Limit error details
385- }
399+ evolved_func = getattr(module, FUNC_NAME)
400+ correct = 0
401+ total = len(TEST_CASES)
402+ errors = []
403+
404+ for input_val, expected in TEST_CASES:
405+ try:
406+ # Handle case where input is a list/mutable - make a copy
407+ if isinstance(input_val, list):
408+ test_input = input_val.copy()
409+ else:
410+ test_input = input_val
411+
412+ result = evolved_func(test_input)
413+ if result == expected:
414+ correct += 1
415+ else:
416+ errors.append(f"Input {{input_val}}: expected {{expected}}, got {{result}}")
417+ except Exception as e:
418+ errors.append(f"Input {{input_val}}: {{str(e)}}")
419+
420+ score = correct / total if total > 0 else 0.0
421+ return {{
422+ "combined_score": score,
423+ "score": score,
424+ "test_pass_rate": score,
425+ "tests_passed": correct,
426+ "total_tests": total,
427+ "errors": errors[:3],
428+ }}
429+ """
386430
387431 return run_evolution (
388- initial_program = func_source , evaluator = evaluator , iterations = iterations , ** kwargs
432+ initial_program = func_source , evaluator = evaluator_code , iterations = iterations , ** kwargs
389433 )
390434
391435
@@ -447,36 +491,51 @@ def benchmark_sort(instance):
447491 lines .append (" " * (indent + 4 ) + "# EVOLVE-BLOCK-END" )
448492 class_source = "\n " .join (lines )
449493
450- # Create evaluator
451- def evaluator (program_path ):
452- import importlib .util
494+ # Create a self-contained evaluator as a code string so it works in subprocesses.
495+ import textwrap
453496
454- # Load the evolved program
455- spec = importlib .util .spec_from_file_location ("evolved" , program_path )
456- if spec is None or spec .loader is None :
457- return {"score" : 0.0 , "error" : "Failed to load program" }
497+ class_name = algorithm_class .__name__
498+ benchmark_source = textwrap .dedent (inspect .getsource (benchmark ))
458499
459- module = importlib .util .module_from_spec (spec )
500+ evaluator_code = f"""
501+ import importlib.util
460502
461- try :
462- spec .loader .exec_module (module )
463- except Exception as e :
464- return {"score" : 0.0 , "error" : f"Failed to execute program: { str (e )} " }
503+ CLASS_NAME = { class_name !r}
465504
466- if not hasattr (module , algorithm_class .__name__ ):
467- return {"score" : 0.0 , "error" : f"Class '{ algorithm_class .__name__ } ' not found" }
505+ { benchmark_source }
468506
469- AlgorithmClass = getattr (module , algorithm_class .__name__ )
507+ def evaluate(program_path):
508+ '''Auto-generated evaluator for evolve_algorithm'''
509+ spec = importlib.util.spec_from_file_location("evolved", program_path)
510+ if spec is None or spec.loader is None:
511+ return {{"combined_score": 0.0, "score": 0.0, "error": "Failed to load program"}}
470512
471- try :
472- instance = AlgorithmClass ()
473- metrics = benchmark (instance )
474- return metrics if isinstance (metrics , dict ) else {"score" : metrics }
475- except Exception as e :
476- return {"score" : 0.0 , "error" : str (e )}
513+ module = importlib.util.module_from_spec(spec)
514+
515+ try:
516+ spec.loader.exec_module(module)
517+ except Exception as e:
518+ return {{"combined_score": 0.0, "score": 0.0, "error": f"Failed to execute program: {{str(e)}}"}}
519+
520+ if not hasattr(module, CLASS_NAME):
521+ return {{"combined_score": 0.0, "score": 0.0, "error": f"Class '{{CLASS_NAME}}' not found"}}
522+
523+ AlgorithmClass = getattr(module, CLASS_NAME)
524+
525+ try:
526+ instance = AlgorithmClass()
527+ metrics = { benchmark .__name__ } (instance)
528+ if not isinstance(metrics, dict):
529+ metrics = {{"score": metrics}}
530+ if "combined_score" not in metrics:
531+ metrics["combined_score"] = metrics.get("score", 0.0)
532+ return metrics
533+ except Exception as e:
534+ return {{"combined_score": 0.0, "score": 0.0, "error": str(e)}}
535+ """
477536
478537 return run_evolution (
479- initial_program = class_source , evaluator = evaluator , iterations = iterations , ** kwargs
538+ initial_program = class_source , evaluator = evaluator_code , iterations = iterations , ** kwargs
480539 )
481540
482541
0 commit comments