diff --git a/openevolve/config.py b/openevolve/config.py index c89766d1c..df859f146 100644 --- a/openevolve/config.py +++ b/openevolve/config.py @@ -32,7 +32,7 @@ class LLMModelConfig: timeout: int = None retries: int = None retry_delay: int = None - + # Reproducibility random_seed: Optional[int] = None @@ -56,10 +56,12 @@ class LLMConfig(LLMModelConfig): retry_delay: int = 5 # n-model configuration for evolution LLM ensemble - models: List[LLMModelConfig] = field(default_factory=lambda: [ - LLMModelConfig(name="gpt-4o-mini", weight=0.8), - LLMModelConfig(name="gpt-4o", weight=0.2) - ]) + models: List[LLMModelConfig] = field( + default_factory=lambda: [ + LLMModelConfig(name="gpt-4o-mini", weight=0.8), + LLMModelConfig(name="gpt-4o", weight=0.2), + ] + ) # n-model configuration for evaluator LLM ensemble evaluator_models: List[LLMModelConfig] = field(default_factory=lambda: []) @@ -264,7 +266,7 @@ def from_dict(cls, config_dict: Dict[str, Any]) -> "Config": config.prompt = PromptConfig(**config_dict["prompt"]) if "database" in config_dict: config.database = DatabaseConfig(**config_dict["database"]) - + # Ensure database inherits the random seed if not explicitly set if config.database.random_seed is None and config.random_seed is not None: config.database.random_seed = config.random_seed @@ -365,4 +367,4 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config: # Make the system message available to the individual models, in case it is not provided from the prompt sampler config.llm.update_model_params({"system_message": config.prompt.system_message}) - return config \ No newline at end of file + return config diff --git a/openevolve/controller.py b/openevolve/controller.py index f9cefe956..0c0ab4d29 100644 --- a/openevolve/controller.py +++ b/openevolve/controller.py @@ -104,20 +104,20 @@ def __init__( # Set global random seeds random.seed(self.config.random_seed) np.random.seed(self.config.random_seed) - + # Create hash-based seeds for different components - base_seed = str(self.config.random_seed).encode('utf-8') - llm_seed = int(hashlib.md5(base_seed + b'llm').hexdigest()[:8], 16) % (2**31) - + base_seed = str(self.config.random_seed).encode("utf-8") + llm_seed = int(hashlib.md5(base_seed + b"llm").hexdigest()[:8], 16) % (2**31) + # Propagate seed to LLM configurations self.config.llm.random_seed = llm_seed for model_cfg in self.config.llm.models: - if not hasattr(model_cfg, 'random_seed') or model_cfg.random_seed is None: + if not hasattr(model_cfg, "random_seed") or model_cfg.random_seed is None: model_cfg.random_seed = llm_seed for model_cfg in self.config.llm.evaluator_models: - if not hasattr(model_cfg, 'random_seed') or model_cfg.random_seed is None: + if not hasattr(model_cfg, "random_seed") or model_cfg.random_seed is None: model_cfg.random_seed = llm_seed - + logger.info(f"Set random seed to {self.config.random_seed} for reproducibility") logger.debug(f"Generated LLM seed: {llm_seed}") @@ -161,7 +161,7 @@ def __init__( self.evaluation_file = evaluation_file logger.info(f"Initialized OpenEvolve with {initial_program_path}") - + # Initialize improved parallel processing components self.parallel_controller = None @@ -212,7 +212,7 @@ async def run( Best program found """ max_iterations = iterations or self.config.max_iterations - + # Determine starting iteration start_iteration = 0 if checkpoint_path and os.path.exists(checkpoint_path): @@ -260,30 +260,31 @@ async def run( self.parallel_controller = ImprovedParallelController( self.config, self.evaluation_file, self.database ) - + # Set up signal handlers for graceful shutdown def signal_handler(signum, frame): logger.info(f"Received signal {signum}, initiating graceful shutdown...") self.parallel_controller.request_shutdown() - + # Set up a secondary handler for immediate exit if user presses Ctrl+C again def force_exit_handler(signum, frame): logger.info("Force exit requested - terminating immediately") import sys + sys.exit(0) - + signal.signal(signal.SIGINT, force_exit_handler) - + signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - + self.parallel_controller.start() - + # Run evolution with improved parallel processing and checkpoint callback await self._run_evolution_with_checkpoints( start_iteration, max_iterations, target_score ) - + finally: # Clean up parallel processing resources if self.parallel_controller: @@ -420,12 +421,10 @@ def _load_checkpoint(self, checkpoint_path: str) -> None: """Load state from a checkpoint directory""" if not os.path.exists(checkpoint_path): raise FileNotFoundError(f"Checkpoint directory {checkpoint_path} not found") - + logger.info(f"Loading checkpoint from {checkpoint_path}") self.database.load(checkpoint_path) - logger.info( - f"Checkpoint loaded successfully (iteration {self.database.last_iteration})" - ) + logger.info(f"Checkpoint loaded successfully (iteration {self.database.last_iteration})") async def _run_evolution_with_checkpoints( self, start_iteration: int, max_iterations: int, target_score: Optional[float] @@ -433,18 +432,17 @@ async def _run_evolution_with_checkpoints( """Run evolution with checkpoint saving support""" logger.info(f"Using island-based evolution with {self.config.database.num_islands} islands") self.database.log_island_status() - + # Run the evolution process with checkpoint callback await self.parallel_controller.run_evolution( - start_iteration, max_iterations, target_score, - checkpoint_callback=self._save_checkpoint + start_iteration, max_iterations, target_score, checkpoint_callback=self._save_checkpoint ) - + # Check if shutdown was requested if self.parallel_controller.shutdown_flag.is_set(): logger.info("Evolution stopped due to shutdown request") return - + # Save final checkpoint if needed final_iteration = start_iteration + max_iterations - 1 if final_iteration > 0 and final_iteration % self.config.checkpoint_interval == 0: @@ -499,4 +497,4 @@ def _save_best_program(self, program: Optional[Program] = None) -> None: indent=2, ) - logger.info(f"Saved best program to {code_path} with program info to {info_path}") \ No newline at end of file + logger.info(f"Saved best program to {code_path} with program info to {info_path}") diff --git a/openevolve/database.py b/openevolve/database.py index f33f994a1..8aba55283 100644 --- a/openevolve/database.py +++ b/openevolve/database.py @@ -9,6 +9,7 @@ import random import time from dataclasses import asdict, dataclass, field, fields + # FileLock removed - no longer needed with threaded parallel processing from typing import Any, Dict, List, Optional, Set, Tuple, Union @@ -198,7 +199,11 @@ def add( # Update archive self._update_archive(program) - # Update the absolute best program tracking + # Enforce population size limit BEFORE updating best program tracking + # This ensures newly added programs aren't immediately removed + self._enforce_population_limit(exclude_program_id=program.id) + + # Update the absolute best program tracking (after population enforcement) self._update_best_program(program) # Save to disk if configured @@ -207,9 +212,6 @@ def add( logger.debug(f"Added program {program.id} to island {island_idx}") - # Enforce population size limit - self._enforce_population_limit() - return program.id def get(self, program_id: str) -> Optional[Program]: @@ -254,9 +256,15 @@ def get_best_program(self, metric: Optional[str] = None) -> Optional[Program]: return None # If no specific metric and we have a tracked best program, return it - if metric is None and self.best_program_id and self.best_program_id in self.programs: - logger.debug(f"Using tracked best program: {self.best_program_id}") - return self.programs[self.best_program_id] + if metric is None and self.best_program_id: + if self.best_program_id in self.programs: + logger.debug(f"Using tracked best program: {self.best_program_id}") + return self.programs[self.best_program_id] + else: + logger.warning( + f"Tracked best program {self.best_program_id} no longer exists, will recalculate" + ) + self.best_program_id = None if metric: # Sort by specific metric @@ -713,7 +721,15 @@ def _update_best_program(self, program: Program) -> None: logger.debug(f"Set initial best program to {program.id}") return - # Compare with current best program + # Compare with current best program (if it still exists) + if self.best_program_id not in self.programs: + logger.warning( + f"Best program {self.best_program_id} no longer exists, clearing reference" + ) + self.best_program_id = program.id + logger.info(f"Set new best program to {program.id}") + return + current_best = self.programs[self.best_program_id] # Update if the new program is better @@ -940,9 +956,12 @@ def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]: return inspirations[:n] - def _enforce_population_limit(self) -> None: + def _enforce_population_limit(self, exclude_program_id: Optional[str] = None) -> None: """ Enforce the population size limit by removing worst programs if needed + + Args: + exclude_program_id: Program ID to never remove (e.g., newly added program) """ if len(self.programs) <= self.config.population_size: return @@ -963,22 +982,24 @@ def _enforce_population_limit(self) -> None: key=lambda p: safe_numeric_average(p.metrics), ) - # Remove worst programs, but never remove the best program + # Remove worst programs, but never remove the best program or excluded program programs_to_remove = [] + protected_ids = {self.best_program_id, exclude_program_id} - {None} + for program in sorted_programs: if len(programs_to_remove) >= num_to_remove: break - # Don't remove the best program - if program.id != self.best_program_id: + # Don't remove the best program or excluded program + if program.id not in protected_ids: programs_to_remove.append(program) - # If we still need to remove more and only have the best program protected, - # remove from the remaining programs anyway (but keep the absolute best) + # If we still need to remove more and only have protected programs, + # remove from the remaining programs anyway (but keep the protected ones) if len(programs_to_remove) < num_to_remove: remaining_programs = [ p for p in sorted_programs - if p not in programs_to_remove and p.id != self.best_program_id + if p not in programs_to_remove and p.id not in protected_ids ] additional_removals = remaining_programs[: num_to_remove - len(programs_to_remove)] programs_to_remove.extend(additional_removals) diff --git a/openevolve/iteration.py b/openevolve/iteration.py index af3d3850d..98db88f09 100644 --- a/openevolve/iteration.py +++ b/openevolve/iteration.py @@ -31,24 +31,21 @@ class Result: artifacts: dict = None - - - async def run_iteration_with_shared_db( - iteration: int, - config: Config, + iteration: int, + config: Config, database: ProgramDatabase, evaluator: Evaluator, llm_ensemble: LLMEnsemble, - prompt_sampler: PromptSampler + prompt_sampler: PromptSampler, ): """ Run a single iteration using shared memory database - + This is optimized for use with persistent worker processes. """ logger = logging.getLogger(__name__) - + try: # Sample parent and inspirations from database parent, inspirations = database.sample() @@ -115,10 +112,10 @@ async def run_iteration_with_shared_db( # Evaluate the child program child_id = str(uuid.uuid4()) result.child_metrics = await evaluator.evaluate_program(child_code, child_id) - + # Handle artifacts if they exist artifacts = evaluator.get_pending_artifacts(child_id) - + # Create a child program result.child_program = Program( id=child_id, @@ -133,13 +130,13 @@ async def run_iteration_with_shared_db( "parent_metrics": parent.metrics, }, ) - + result.prompt = prompt result.llm_response = llm_response result.artifacts = artifacts result.iteration_time = time.time() - iteration_start result.iteration = iteration - + return result except Exception as e: diff --git a/openevolve/llm/ensemble.py b/openevolve/llm/ensemble.py index fa11e28f8..f5db7be2b 100644 --- a/openevolve/llm/ensemble.py +++ b/openevolve/llm/ensemble.py @@ -27,16 +27,22 @@ def __init__(self, models_cfg: List[LLMModelConfig]): self.weights = [model.weight for model in models_cfg] total = sum(self.weights) self.weights = [w / total for w in self.weights] - + # Set up random state for deterministic model selection self.random_state = random.Random() # Initialize with seed from first model's config if available - if models_cfg and hasattr(models_cfg[0], 'random_seed') and models_cfg[0].random_seed is not None: + if ( + models_cfg + and hasattr(models_cfg[0], "random_seed") + and models_cfg[0].random_seed is not None + ): self.random_state.seed(models_cfg[0].random_seed) - logger.debug(f"LLMEnsemble: Set random seed to {models_cfg[0].random_seed} for deterministic model selection") + logger.debug( + f"LLMEnsemble: Set random seed to {models_cfg[0].random_seed} for deterministic model selection" + ) # Only log if we have multiple models or this is the first ensemble - if len(models_cfg) > 1 or not hasattr(logger, '_ensemble_logged'): + if len(models_cfg) > 1 or not hasattr(logger, "_ensemble_logged"): logger.info( f"Initialized LLM ensemble with models: " + ", ".join( diff --git a/openevolve/llm/openai.py b/openevolve/llm/openai.py index 705d0d4ac..7946b4d81 100644 --- a/openevolve/llm/openai.py +++ b/openevolve/llm/openai.py @@ -32,7 +32,7 @@ def __init__( self.retry_delay = model_cfg.retry_delay self.api_base = model_cfg.api_base self.api_key = model_cfg.api_key - self.random_seed = getattr(model_cfg, 'random_seed', None) + self.random_seed = getattr(model_cfg, "random_seed", None) # Set up API client self.client = openai.OpenAI( @@ -41,9 +41,9 @@ def __init__( ) # Only log unique models to reduce duplication - if not hasattr(logger, '_initialized_models'): + if not hasattr(logger, "_initialized_models"): logger._initialized_models = set() - + if self.model not in logger._initialized_models: logger.info(f"Initialized OpenAI LLM with model: {self.model}") logger._initialized_models.add(self.model) @@ -80,7 +80,7 @@ async def generate_with_context( "top_p": kwargs.get("top_p", self.top_p), "max_tokens": kwargs.get("max_tokens", self.max_tokens), } - + # Add seed parameter for reproducibility if configured # Skip seed parameter for Google AI Studio endpoint as it doesn't support it seed = kwargs.get("seed", self.random_seed) diff --git a/openevolve/prompt/sampler.py b/openevolve/prompt/sampler.py index 44b8acfb3..af0d2eb04 100644 --- a/openevolve/prompt/sampler.py +++ b/openevolve/prompt/sampler.py @@ -29,7 +29,7 @@ def __init__(self, config: PromptConfig): self.user_template_override = None # Only log once to reduce duplication - if not hasattr(logger, '_prompt_sampler_logged'): + if not hasattr(logger, "_prompt_sampler_logged"): logger.info("Initialized prompt sampler") logger._prompt_sampler_logged = True @@ -412,39 +412,39 @@ def _format_inspirations_section( ) -> str: """ Format the inspirations section for the prompt - + Args: inspirations: List of inspiration programs language: Programming language - + Returns: Formatted inspirations section string """ if not inspirations: return "" - + # Get templates inspirations_section_template = self.template_manager.get_template("inspirations_section") inspiration_program_template = self.template_manager.get_template("inspiration_program") - + inspiration_programs_str = "" - + for i, program in enumerate(inspirations): # Extract a snippet (first 8 lines) for display program_code = program.get("code", "") program_snippet = "\n".join(program_code.split("\n")[:8]) if len(program_code.split("\n")) > 8: program_snippet += "\n# ... (truncated for brevity)" - + # Calculate a composite score using safe numeric average score = safe_numeric_average(program.get("metrics", {})) - + # Determine program type based on metadata and score program_type = self._determine_program_type(program) - + # Extract unique features (emphasizing diversity rather than just performance) unique_features = self._extract_unique_features(program) - + inspiration_programs_str += ( inspiration_program_template.format( program_number=i + 1, @@ -456,24 +456,24 @@ def _format_inspirations_section( ) + "\n\n" ) - + return inspirations_section_template.format( inspiration_programs=inspiration_programs_str.strip() ) - + def _determine_program_type(self, program: Dict[str, Any]) -> str: """ Determine the type/category of an inspiration program - + Args: program: Program dictionary - + Returns: String describing the program type """ metadata = program.get("metadata", {}) score = safe_numeric_average(program.get("metrics", {})) - + # Check metadata for explicit type markers if metadata.get("diverse", False): return "Diverse" @@ -481,7 +481,7 @@ def _determine_program_type(self, program: Dict[str, Any]) -> str: return "Migrant" if metadata.get("random", False): return "Random" - + # Classify based on score ranges if score >= 0.8: return "High-Performer" @@ -491,26 +491,26 @@ def _determine_program_type(self, program: Dict[str, Any]) -> str: return "Experimental" else: return "Exploratory" - + def _extract_unique_features(self, program: Dict[str, Any]) -> str: """ Extract unique features of an inspiration program - + Args: program: Program dictionary - + Returns: String describing unique aspects of the program """ features = [] - + # Extract from metadata if available metadata = program.get("metadata", {}) if "changes" in metadata: changes = metadata["changes"] if isinstance(changes, str) and len(changes) < 100: features.append(f"Modification: {changes}") - + # Analyze metrics for standout characteristics metrics = program.get("metrics", {}) for metric_name, value in metrics.items(): @@ -519,7 +519,7 @@ def _extract_unique_features(self, program: Dict[str, Any]) -> str: features.append(f"Excellent {metric_name} ({value:.3f})") elif value <= 0.3: features.append(f"Alternative {metric_name} approach") - + # Code-based features (simple heuristics) code = program.get("code", "") if code: @@ -534,12 +534,12 @@ def _extract_unique_features(self, program: Dict[str, Any]) -> str: features.append("Concise implementation") elif len(code.split("\n")) > 50: features.append("Comprehensive implementation") - + # Default if no specific features found if not features: program_type = self._determine_program_type(program) features.append(f"{program_type} approach to the problem") - + return ", ".join(features[:3]) # Limit to top 3 features def _apply_template_variations(self, template: str) -> str: diff --git a/openevolve/threaded_parallel.py b/openevolve/threaded_parallel.py index e772d6920..815de9689 100644 --- a/openevolve/threaded_parallel.py +++ b/openevolve/threaded_parallel.py @@ -23,92 +23,93 @@ class ThreadedEvaluationPool: """ Thread-based parallel evaluation pool for improved performance - + Uses threads instead of processes to avoid pickling issues while still providing parallelism for I/O-bound LLM calls. """ - + def __init__(self, config: Config, evaluation_file: str, database: ProgramDatabase): self.config = config self.evaluation_file = evaluation_file self.database = database - + self.num_workers = config.evaluator.parallel_evaluations self.executor = None - + # Pre-initialize components for each thread self.thread_local = threading.local() - + logger.info(f"Initializing threaded evaluation pool with {self.num_workers} workers") - + def start(self) -> None: """Start the thread pool""" self.executor = ThreadPoolExecutor( - max_workers=self.num_workers, - thread_name_prefix="EvalWorker" + max_workers=self.num_workers, thread_name_prefix="EvalWorker" ) logger.info(f"Started thread pool with {self.num_workers} threads") - + def stop(self) -> None: """Stop the thread pool""" if self.executor: self.executor.shutdown(wait=True) self.executor = None logger.info("Stopped thread pool") - + def submit_evaluation(self, iteration: int) -> Future: """ Submit an evaluation task to the thread pool - + Args: iteration: Iteration number to evaluate - + Returns: Future that will contain the result """ if not self.executor: raise RuntimeError("Thread pool not started") - + return self.executor.submit(self._run_evaluation, iteration) - + def _run_evaluation(self, iteration: int): """Run evaluation in a worker thread""" # Get or create thread-local components - if not hasattr(self.thread_local, 'initialized'): + if not hasattr(self.thread_local, "initialized"): self._initialize_thread_components() - + try: # Run the iteration - result = asyncio.run(run_iteration_with_shared_db( - iteration, - self.config, - self.database, # Shared database (thread-safe reads) - self.thread_local.evaluator, - self.thread_local.llm_ensemble, - self.thread_local.prompt_sampler - )) - + result = asyncio.run( + run_iteration_with_shared_db( + iteration, + self.config, + self.database, # Shared database (thread-safe reads) + self.thread_local.evaluator, + self.thread_local.llm_ensemble, + self.thread_local.prompt_sampler, + ) + ) + return result - + except Exception as e: logger.error(f"Error in thread evaluation {iteration}: {e}") return None - + def _initialize_thread_components(self) -> None: """Initialize components for this thread""" thread_id = threading.get_ident() logger.debug(f"Initializing components for thread {thread_id}") - + try: # Initialize LLM components self.thread_local.llm_ensemble = LLMEnsemble(self.config.llm.models) self.thread_local.llm_evaluator_ensemble = LLMEnsemble(self.config.llm.evaluator_models) - + # Initialize prompt samplers self.thread_local.prompt_sampler = PromptSampler(self.config.prompt) self.thread_local.evaluator_prompt_sampler = PromptSampler(self.config.prompt) self.thread_local.evaluator_prompt_sampler.set_templates("evaluator_system_message") - + # Initialize evaluator self.thread_local.evaluator = Evaluator( self.config.evaluator, @@ -117,10 +118,10 @@ def _initialize_thread_components(self) -> None: self.thread_local.evaluator_prompt_sampler, database=self.database, ) - + self.thread_local.initialized = True logger.debug(f"Initialized components for thread {thread_id}") - + except Exception as e: logger.error(f"Failed to initialize thread components: {e}") raise @@ -130,135 +131,146 @@ class ImprovedParallelController: """ Controller for improved parallel processing using shared memory and threads """ - + def __init__(self, config: Config, evaluation_file: str, database: ProgramDatabase): self.config = config self.evaluation_file = evaluation_file self.database = database - + self.thread_pool = None self.database_lock = threading.RLock() # For database writes self.shutdown_flag = threading.Event() # For graceful shutdown - + def start(self) -> None: """Start the improved parallel system""" - self.thread_pool = ThreadedEvaluationPool( - self.config, self.evaluation_file, self.database - ) + self.thread_pool = ThreadedEvaluationPool(self.config, self.evaluation_file, self.database) self.thread_pool.start() - + logger.info("Started improved parallel controller") - + def stop(self) -> None: """Stop the improved parallel system""" self.shutdown_flag.set() # Signal shutdown - + if self.thread_pool: self.thread_pool.stop() self.thread_pool = None - + logger.info("Stopped improved parallel controller") - + def request_shutdown(self) -> None: """Request graceful shutdown (for signal handlers)""" logger.info("Graceful shutdown requested...") self.shutdown_flag.set() - + async def run_evolution( - self, start_iteration: int, max_iterations: int, target_score: Optional[float] = None, - checkpoint_callback=None + self, + start_iteration: int, + max_iterations: int, + target_score: Optional[float] = None, + checkpoint_callback=None, ): """ Run evolution with improved parallel processing - + Args: start_iteration: Starting iteration number max_iterations: Maximum number of iterations target_score: Target score to achieve - + Returns: Best program found """ total_iterations = start_iteration + max_iterations - + logger.info( f"Starting improved parallel evolution from iteration {start_iteration} " f"for {max_iterations} iterations (total: {total_iterations})" ) - + # Submit initial batch of evaluations pending_futures = {} batch_size = min(self.config.evaluator.parallel_evaluations * 2, max_iterations) - + for i in range(start_iteration, min(start_iteration + batch_size, total_iterations)): future = self.thread_pool.submit_evaluation(i) pending_futures[i] = future - + next_iteration_to_submit = start_iteration + batch_size completed_iterations = 0 - + # Island management programs_per_island = max(1, max_iterations // (self.config.database.num_islands * 10)) current_island_counter = 0 - + # Process results as they complete - while pending_futures and completed_iterations < max_iterations and not self.shutdown_flag.is_set(): + while ( + pending_futures + and completed_iterations < max_iterations + and not self.shutdown_flag.is_set() + ): # Find completed futures completed_iteration = None for iteration, future in list(pending_futures.items()): if future.done(): completed_iteration = iteration break - + if completed_iteration is None: # No results ready, wait a bit await asyncio.sleep(0.01) continue - + # Process completed result future = pending_futures.pop(completed_iteration) - + try: result = future.result() - - if result and hasattr(result, 'child_program') and result.child_program: + + if result and hasattr(result, "child_program") and result.child_program: # Thread-safe database update with self.database_lock: self.database.add(result.child_program, iteration=completed_iteration) - + # Store artifacts if they exist if result.artifacts: self.database.store_artifacts(result.child_program.id, result.artifacts) - + # Log prompts - if hasattr(result, 'prompt') and result.prompt: + if hasattr(result, "prompt") and result.prompt: self.database.log_prompt( template_key=( - "full_rewrite_user" if not self.config.diff_based_evolution + "full_rewrite_user" + if not self.config.diff_based_evolution else "diff_user" ), program_id=result.child_program.id, prompt=result.prompt, - responses=[result.llm_response] if hasattr(result, 'llm_response') else [], + responses=( + [result.llm_response] if hasattr(result, "llm_response") else [] + ), ) - + # Manage island evolution - if completed_iteration > start_iteration and current_island_counter >= programs_per_island: + if ( + completed_iteration > start_iteration + and current_island_counter >= programs_per_island + ): self.database.next_island() current_island_counter = 0 logger.debug(f"Switched to island {self.database.current_island}") - + current_island_counter += 1 - + # Increment generation for current island self.database.increment_island_generation() - + # Check migration if self.database.should_migrate(): logger.info(f"Performing migration at iteration {completed_iteration}") self.database.migrate_programs() self.database.log_island_status() - + # Log progress (outside lock) logger.info( f"Iteration {completed_iteration}: " @@ -266,32 +278,37 @@ async def run_evolution( f"(parent: {result.parent.id if result.parent else 'None'}) " f"completed in {result.iteration_time:.2f}s" ) - + if result.child_program.metrics: - metrics_str = ", ".join([ - f"{k}={v:.4f}" if isinstance(v, (int, float)) else f"{k}={v}" - for k, v in result.child_program.metrics.items() - ]) + metrics_str = ", ".join( + [ + f"{k}={v:.4f}" if isinstance(v, (int, float)) else f"{k}={v}" + for k, v in result.child_program.metrics.items() + ] + ) logger.info(f"Metrics: {metrics_str}") - + # Check for new best program if self.database.best_program_id == result.child_program.id: logger.info( f"🌟 New best solution found at iteration {completed_iteration}: " f"{result.child_program.id}" ) - + # Save checkpoints at intervals if completed_iteration % self.config.checkpoint_interval == 0: - logger.info(f"Checkpoint interval reached at iteration {completed_iteration}") + logger.info( + f"Checkpoint interval reached at iteration {completed_iteration}" + ) self.database.log_island_status() if checkpoint_callback: checkpoint_callback(completed_iteration) - + # Check target score if target_score is not None and result.child_program.metrics: numeric_metrics = [ - v for v in result.child_program.metrics.values() + v + for v in result.child_program.metrics.values() if isinstance(v, (int, float)) ] if numeric_metrics: @@ -303,18 +320,18 @@ async def run_evolution( break else: logger.warning(f"No valid result from iteration {completed_iteration}") - + except Exception as e: logger.error(f"Error processing result from iteration {completed_iteration}: {e}") - + completed_iterations += 1 - + # Submit next iteration if available if next_iteration_to_submit < total_iterations: future = self.thread_pool.submit_evaluation(next_iteration_to_submit) pending_futures[next_iteration_to_submit] = future next_iteration_to_submit += 1 - + # Handle shutdown or completion if self.shutdown_flag.is_set(): logger.info("Shutdown requested, canceling remaining evaluations...") @@ -329,8 +346,8 @@ async def run_evolution( future.result(timeout=10.0) except Exception as e: logger.warning(f"Error waiting for iteration {iteration}: {e}") - + if self.shutdown_flag.is_set(): logger.info("Evolution interrupted by shutdown") else: - logger.info("Evolution completed") \ No newline at end of file + logger.info("Evolution completed") diff --git a/pyproject.toml b/pyproject.toml index d73b29612..abe90c44f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "openevolve" -version = "0.0.13" +version = "0.0.14" description = "Open-source implementation of AlphaEvolve" readme = "README.md" requires-python = ">=3.9" diff --git a/setup.py b/setup.py index d3a277533..e876b1c90 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="openevolve", - version="0.0.13", + version="0.0.14", packages=find_packages(), include_package_data=True, )