Buckets:
tostido/Butterfly-Field-Station-storage / work /Convergence_Engine /reality_simulator /evolution_engine.py
| """ | |
| 🧬 GENETIC EVOLUTION ENGINE (Layer 2) | |
| Darwinian evolution through genetic algorithms - where quantum particles | |
| become organisms with heritable traits, fitness evaluation, and natural selection. | |
| Features: | |
| - BitArray genotypes for memory efficiency | |
| - Recursive genotype → phenotype mapping | |
| - Adaptive mutation rates based on fitness landscape | |
| - Cached fitness evaluation | |
| - Generational batching for performance | |
| - Tournament selection and elitism | |
| """ | |
| import numpy as np | |
| from typing import List, Dict, Tuple, Optional, Callable, Any | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| import time | |
| import hashlib | |
| from collections import Counter | |
| import math | |
| class GenotypeEncoding(Enum): | |
| """Different ways to encode genetic information""" | |
| BIT_ARRAY = "bit_array" # Binary string | |
| REAL_VALUED = "real_valued" # Floating point genes | |
| INTEGER = "integer" # Integer genes | |
| MIXED = "mixed" # Combination of types | |
| class Genotype: | |
| """ | |
| Genetic representation of an organism | |
| Uses efficient bit array encoding for memory optimization | |
| Now includes consciousness emergence traits | |
| """ | |
| genes: np.ndarray # Bit array (uint8 for memory efficiency) | |
| encoding: GenotypeEncoding = GenotypeEncoding.BIT_ARRAY | |
| fitness: Optional[float] = None | |
| age: int = 0 | |
| generation: int = 0 | |
| def __post_init__(self): | |
| if self.encoding == GenotypeEncoding.BIT_ARRAY: | |
| # Ensure genes are binary | |
| self.genes = (self.genes > 0).astype(np.uint8) | |
| def get_hash(self) -> str: | |
| """Get unique hash for this genotype (for caching)""" | |
| return hashlib.md5(self.genes.tobytes()).hexdigest()[:16] | |
| def copy(self) -> 'Genotype': | |
| """Create a deep copy""" | |
| return Genotype( | |
| genes=self.genes.copy(), | |
| encoding=self.encoding, | |
| fitness=self.fitness, | |
| age=self.age + 1, | |
| generation=self.generation | |
| ) | |
| def mutate(self, rate: float = 0.01) -> 'Genotype': | |
| """Apply random mutations""" | |
| if self.encoding == GenotypeEncoding.BIT_ARRAY: | |
| return self._mutate_bit_array(rate) | |
| else: | |
| return self._mutate_real_valued(rate) | |
| def _mutate_bit_array(self, rate: float) -> 'Genotype': | |
| """Bit flip mutations for binary encoding""" | |
| mutated = self.copy() | |
| # Flip bits with given probability | |
| mutation_mask = np.random.random(len(self.genes)) < rate | |
| mutated.genes = (mutated.genes + mutation_mask.astype(np.uint8)) % 2 | |
| return mutated | |
| def _mutate_real_valued(self, rate: float) -> 'Genotype': | |
| """Gaussian mutations for real-valued genes""" | |
| mutated = self.copy() | |
| # Add Gaussian noise to genes | |
| noise = np.random.normal(0, 0.1, len(self.genes)) | |
| mutation_mask = np.random.random(len(self.genes)) < rate | |
| mutated.genes = mutated.genes + (noise * mutation_mask) | |
| # Clamp to reasonable bounds | |
| mutated.genes = np.clip(mutated.genes, 0.0, 1.0) | |
| return mutated | |
| def crossover(self, other: 'Genotype', method: str = 'single_point') -> Tuple['Genotype', 'Genotype']: | |
| """Crossover with another genotype""" | |
| if self.encoding == GenotypeEncoding.BIT_ARRAY: | |
| return self._crossover_bit_array(other, method) | |
| else: | |
| return self._crossover_real_valued(other) | |
| def _crossover_bit_array(self, other: 'Genotype', method: str) -> Tuple['Genotype', 'Genotype']: | |
| """Single/multi-point crossover for binary genes""" | |
| if method == 'uniform': | |
| # Uniform crossover | |
| mask = np.random.random(len(self.genes)) > 0.5 | |
| child1_genes = np.where(mask, self.genes, other.genes) | |
| child2_genes = np.where(mask, other.genes, self.genes) | |
| else: | |
| # Single-point crossover | |
| point = np.random.randint(1, len(self.genes)) | |
| child1_genes = np.concatenate([self.genes[:point], other.genes[point:]]) | |
| child2_genes = np.concatenate([other.genes[:point], self.genes[point:]]) | |
| child1 = Genotype(genes=child1_genes, encoding=self.encoding, | |
| generation=max(self.generation, other.generation) + 1) | |
| child2 = Genotype(genes=child2_genes, encoding=self.encoding, | |
| generation=max(self.generation, other.generation) + 1) | |
| return child1, child2 | |
| def _crossover_real_valued(self, other: 'Genotype') -> Tuple['Genotype', 'Genotype']: | |
| """Blend crossover for real-valued genes""" | |
| alpha = np.random.uniform(0.1, 0.9) | |
| child1_genes = alpha * self.genes + (1 - alpha) * other.genes | |
| child2_genes = (1 - alpha) * self.genes + alpha * other.genes | |
| child1 = Genotype(genes=child1_genes, encoding=self.encoding, | |
| generation=max(self.generation, other.generation) + 1) | |
| child2 = Genotype(genes=child2_genes, encoding=self.encoding, | |
| generation=max(self.generation, other.generation) + 1) | |
| return child1, child2 | |
| class Phenotype: | |
| """ | |
| Observable traits and characteristics of an organism | |
| Maps genotype to expressed traits through recursive development | |
| Now includes consciousness emergence phenotypes and cognitive traits | |
| """ | |
| traits: Dict[str, float] = field(default_factory=dict) | |
| development_stage: int = 0 | |
| environmental_factors: Dict[str, float] = field(default_factory=dict) | |
| # 🧬 NAMED COGNITIVE TRAITS - heritable, affect learning behavior | |
| # Curiosity: affects entropy bonus during language training (prevents mode collapse) | |
| # High curiosity = more exploration = more diverse language output | |
| curiosity: float = 0.5 # Range [0, 1], default moderate curiosity | |
| def express_trait(self, trait_name: str, genotype_value: float, | |
| environmental_modifier: float = 1.0) -> float: | |
| """Express a trait from genotype with environmental influence""" | |
| # Base expression | |
| base_value = genotype_value | |
| # Environmental modification | |
| modified_value = base_value * environmental_modifier | |
| # Development stage affects expression (default to 1.0 if development_stage is 0) | |
| if self.development_stage == 0: | |
| development_factor = 1.0 | |
| else: | |
| development_factor = min(1.0, self.development_stage / 10.0) | |
| final_value = modified_value * development_factor | |
| # Store the trait | |
| self.traits[trait_name] = final_value | |
| return final_value | |
| def get_fitness_contribution(self, trait_name: str, target_value: float) -> float: | |
| """Calculate how well this trait contributes to fitness""" | |
| if trait_name not in self.traits: | |
| return 0.0 | |
| actual_value = self.traits[trait_name] | |
| deviation = abs(actual_value - target_value) | |
| # Fitness decreases with deviation (Gaussian fitness) | |
| return np.exp(-deviation**2) | |
| class Organism: | |
| """ | |
| Complete organism with genotype, phenotype, and evolutionary history | |
| """ | |
| genotype: Genotype | |
| phenotype: Phenotype = field(default_factory=Phenotype) | |
| fitness: float = 0.0 | |
| species_id: Optional[str] = None | |
| parent_ids: List[str] = field(default_factory=list) | |
| def __post_init__(self): | |
| if self.species_id is None: | |
| self.species_id = self.genotype.get_hash() | |
| def develop_phenotype(self, environmental_factors: Dict[str, float] = None): | |
| """Develop phenotype from genotype with consciousness emergence""" | |
| if environmental_factors: | |
| self.phenotype.environmental_factors.update(environmental_factors) | |
| # Express traditional traits from genotype genes | |
| num_traits = min(len(self.genotype.genes), 10) # Limit traits for simplicity | |
| for i in range(num_traits): | |
| trait_name = f"trait_{i}" | |
| genotype_value = self.genotype.genes[i] / 255.0 # Normalize to [0, 1] | |
| # Environmental modifier (default 1.0 if no environmental factor) | |
| env_modifier = self.phenotype.environmental_factors.get(trait_name, 1.0) | |
| expressed_value = self.phenotype.express_trait( | |
| trait_name, genotype_value, env_modifier | |
| ) | |
| # 🧬 EXPRESS COGNITIVE TRAITS FROM SPECIFIC GENE POSITIONS | |
| # For bit-array genomes: use genes 10-17 (8 bits) to encode curiosity | |
| # For real-valued genomes: use gene 10 directly | |
| # This affects language exploration (entropy bonus) and is heritable | |
| if len(self.genotype.genes) >= 11: | |
| if self.genotype.encoding == GenotypeEncoding.BIT_ARRAY: | |
| # Use 8 bits starting at gene 10 to create a 0-255 value | |
| start_idx = min(10, len(self.genotype.genes) - 8) | |
| if start_idx >= 0 and start_idx + 8 <= len(self.genotype.genes): | |
| # Convert 8 bits to byte value (0-255) | |
| bit_value = sum(int(self.genotype.genes[start_idx + i]) << i for i in range(8)) | |
| base_curiosity = bit_value / 255.0 | |
| else: | |
| # Not enough genes, use average of available bits | |
| base_curiosity = float(self.genotype.genes[10:].mean()) if len(self.genotype.genes) > 10 else 0.5 | |
| else: | |
| # Real-valued or integer encoding - use directly | |
| base_curiosity = self.genotype.genes[10] / 255.0 if self.genotype.genes[10] <= 255 else self.genotype.genes[10] | |
| # Environmental modifier for curiosity (e.g., rich environment = more curious) | |
| curiosity_env = self.phenotype.environmental_factors.get('curiosity', 1.0) | |
| # Apply development stage: young organisms start more curious | |
| development_bonus = max(0, 0.2 - (self.phenotype.development_stage * 0.02)) | |
| self.phenotype.curiosity = min(1.0, base_curiosity * curiosity_env + development_bonus) | |
| self.phenotype.traits['curiosity'] = self.phenotype.curiosity | |
| # Increase development stage | |
| self.phenotype.development_stage += 1 | |
| def calculate_fitness(self, fitness_targets: Dict[str, float]) -> float: | |
| """Calculate overall fitness based on trait targets including consciousness""" | |
| total_fitness = 0.0 | |
| # Traditional trait fitness | |
| for trait_name, target_value in fitness_targets.items(): | |
| contribution = self.phenotype.get_fitness_contribution(trait_name, target_value) | |
| total_fitness += contribution | |
| # Normalize by number of traits | |
| if fitness_targets: | |
| total_fitness /= len(fitness_targets) | |
| # Allow fitness to exceed 1.0 for meaningful evolution | |
| # total_fitness = np.clip(total_fitness, 0.0, 1.0) # REMOVED - let fitness be unbounded | |
| # Apply high-precision rounding based on config | |
| fitness_precision = getattr(self, 'fitness_precision', 0.000001) | |
| total_fitness = round(total_fitness / fitness_precision) * fitness_precision | |
| self.fitness = total_fitness | |
| self.genotype.fitness = total_fitness | |
| return total_fitness | |
| class FitnessCache: | |
| """ | |
| Caches fitness calculations to avoid recomputation | |
| Uses genotype hash for efficient lookup | |
| """ | |
| def __init__(self, max_cache_size: int = 10000): | |
| self.cache: Dict[str, float] = {} | |
| self.max_cache_size = max_cache_size | |
| self.access_order: List[str] = [] # For LRU eviction | |
| def get(self, genotype_hash: str) -> Optional[float]: | |
| """Get cached fitness value""" | |
| if genotype_hash in self.cache: | |
| # Move to end (most recently used) | |
| self.access_order.remove(genotype_hash) | |
| self.access_order.append(genotype_hash) | |
| return self.cache[genotype_hash] | |
| return None | |
| def put(self, genotype_hash: str, fitness: float): | |
| """Cache fitness value""" | |
| if genotype_hash in self.cache: | |
| # Update existing | |
| self.access_order.remove(genotype_hash) | |
| elif len(self.cache) >= self.max_cache_size: | |
| # Evict least recently used | |
| evicted = self.access_order.pop(0) | |
| del self.cache[evicted] | |
| self.cache[genotype_hash] = fitness | |
| self.access_order.append(genotype_hash) | |
| def clear(self): | |
| """Clear all cached values""" | |
| self.cache.clear() | |
| self.access_order.clear() | |
| def stats(self) -> Dict[str, int]: | |
| """Get cache statistics""" | |
| return { | |
| 'size': len(self.cache), | |
| 'max_size': self.max_cache_size, | |
| 'hit_rate_estimate': 0 # Would need hit/miss tracking | |
| } | |
| class SelectionEngine: | |
| """ | |
| Handles parent selection and survival using various strategies | |
| """ | |
| def __init__(self, tournament_size: int = 5, elitism_rate: float = 0.1): | |
| self.tournament_size = tournament_size | |
| self.elitism_rate = elitism_rate | |
| def tournament_selection(self, population: List[Organism], num_parents: int) -> List[Organism]: | |
| """Tournament selection for parent selection""" | |
| parents = [] | |
| for _ in range(num_parents): | |
| # Select random tournament participants | |
| tournament = np.random.choice(population, self.tournament_size, replace=False) | |
| # Find winner (highest fitness) | |
| winner = max(tournament, key=lambda org: org.fitness) | |
| parents.append(winner) | |
| return parents | |
| def elitism_selection(self, population: List[Organism], num_elites: int) -> List[Organism]: | |
| """Select top performers for direct survival""" | |
| sorted_pop = sorted(population, key=lambda org: org.fitness, reverse=True) | |
| return sorted_pop[:num_elites] | |
| def rank_based_selection(self, population: List[Organism], num_selected: int) -> List[Organism]: | |
| """Rank-based selection (fitness proportional to rank)""" | |
| sorted_pop = sorted(population, key=lambda org: org.fitness, reverse=True) | |
| # Calculate selection probabilities based on rank | |
| ranks = np.arange(1, len(sorted_pop) + 1) | |
| probabilities = 1.0 / ranks # Higher rank = higher probability | |
| probabilities /= probabilities.sum() | |
| # Select based on probabilities | |
| selected_indices = np.random.choice(len(sorted_pop), num_selected, p=probabilities) | |
| selected = [sorted_pop[i] for i in selected_indices] | |
| return selected | |
| class MutationEngine: | |
| """ | |
| Handles mutation operations with adaptive rates | |
| """ | |
| def __init__(self, base_rate: float = 0.01, adaptive: bool = True): | |
| self.base_rate = base_rate | |
| self.adaptive = adaptive | |
| self.generation_stats = { | |
| 'avg_fitness': [], | |
| 'mutation_rates': [] | |
| } | |
| def get_adaptive_rate(self, population: List[Organism], current_gen: int) -> float: | |
| """Calculate adaptive mutation rate based on population diversity and progress""" | |
| if not self.adaptive or len(self.generation_stats['avg_fitness']) < 2: | |
| return self.base_rate | |
| # Calculate fitness trend | |
| recent_fitness = self.generation_stats['avg_fitness'][-5:] # Last 5 generations | |
| fitness_trend = np.polyfit(range(len(recent_fitness)), recent_fitness, 1)[0] | |
| # Calculate population diversity (coefficient of variation) | |
| fitnesses = [org.fitness for org in population] | |
| diversity = np.std(fitnesses) / (np.mean(fitnesses) + 1e-10) | |
| # Adaptive rate calculation | |
| if fitness_trend < 0.001: # Stagnation | |
| rate = self.base_rate * 2.0 # Increase exploration | |
| elif diversity < 0.1: # Low diversity | |
| rate = self.base_rate * 1.5 # Increase variation | |
| elif fitness_trend > 0.01: # Strong progress | |
| rate = self.base_rate * 0.8 # Decrease disruption | |
| else: | |
| rate = self.base_rate # Maintain baseline | |
| # Clamp to reasonable bounds | |
| rate = np.clip(rate, 0.001, 0.1) | |
| self.generation_stats['mutation_rates'].append(rate) | |
| return rate | |
| def apply_mutations(self, offspring: List[Genotype], rate: float) -> List[Genotype]: | |
| """Apply mutations to a generation of offspring""" | |
| mutated = [] | |
| for genotype in offspring: | |
| if np.random.random() < 0.3: # 30% chance of mutation | |
| mutated_genotype = genotype.mutate(rate) | |
| mutated.append(mutated_genotype) | |
| else: | |
| mutated.append(genotype) | |
| return mutated | |
| def update_stats(self, population: List[Organism]): | |
| """Update generation statistics""" | |
| avg_fitness = np.mean([org.fitness for org in population]) | |
| self.generation_stats['avg_fitness'].append(avg_fitness) | |
| # Keep only recent stats | |
| max_stats = 50 | |
| if len(self.generation_stats['avg_fitness']) > max_stats: | |
| self.generation_stats['avg_fitness'] = self.generation_stats['avg_fitness'][-max_stats:] | |
| if len(self.generation_stats['mutation_rates']) > max_stats: | |
| self.generation_stats['mutation_rates'] = self.generation_stats['mutation_rates'][-max_stats:] | |
| class DiversityGuard: | |
| """ | |
| Prevents premature convergence through diversity enforcement | |
| Tracks genotype frequencies and applies fitness penalties to over-represented genotypes. | |
| """ | |
| def __init__(self, | |
| hash_similarity_threshold: float = 0.92, | |
| penalty: float = 0.05, | |
| frequency_threshold: float = 0.1, | |
| enabled: bool = True): | |
| self.hash_similarity_threshold = hash_similarity_threshold | |
| self.penalty = penalty | |
| self.frequency_threshold = frequency_threshold | |
| self.enabled = enabled | |
| # Track genotype frequencies per generation | |
| self.genotype_counts: Counter = Counter() | |
| self.generation_history: List[Dict[str, int]] = [] | |
| def calculate_genotype_similarity(self, hash1: str, hash2: str) -> float: | |
| """Calculate similarity between two genotype hashes (0.0-1.0)""" | |
| if len(hash1) != len(hash2): | |
| return 0.0 | |
| matches = sum(c1 == c2 for c1, c2 in zip(hash1, hash2)) | |
| return matches / len(hash1) if len(hash1) > 0 else 0.0 | |
| def find_similar_genotypes(self, target_hash: str, population_hashes: List[str]) -> List[str]: | |
| """Find genotypes similar to target (above threshold)""" | |
| similar = [] | |
| for hash_val in population_hashes: | |
| similarity = self.calculate_genotype_similarity(target_hash, hash_val) | |
| if similarity >= self.hash_similarity_threshold: | |
| similar.append(hash_val) | |
| return similar | |
| def apply_diversity_penalty(self, organism, population: List) -> float: | |
| """Apply fitness penalty based on genotype frequency""" | |
| if not self.enabled or not population: | |
| return 0.0 | |
| # Get organism's genotype hash | |
| genotype_hash = organism.genotype.get_hash() | |
| # Count similar genotypes in population | |
| population_hashes = [org.genotype.get_hash() for org in population] | |
| similar_count = len(self.find_similar_genotypes(genotype_hash, population_hashes)) | |
| # Calculate frequency | |
| total_population = len(population) | |
| frequency = similar_count / total_population if total_population > 0 else 0.0 | |
| # Apply penalty if frequency exceeds threshold | |
| if frequency > self.frequency_threshold: | |
| # Penalty increases with frequency | |
| penalty_multiplier = min(frequency / 0.5, 1.0) # Max penalty at 50% frequency | |
| return self.penalty * penalty_multiplier | |
| return 0.0 | |
| def update_generation(self, population: List): | |
| """Track genotype frequencies for this generation""" | |
| self.genotype_counts.clear() | |
| for org in population: | |
| hash_val = org.genotype.get_hash() | |
| self.genotype_counts[hash_val] += 1 | |
| # Store generation snapshot | |
| self.generation_history.append(dict(self.genotype_counts)) | |
| # Keep only last 10 generations | |
| if len(self.generation_history) > 10: | |
| self.generation_history.pop(0) | |
| def get_diversity_metrics(self) -> Dict[str, float]: | |
| """Get current diversity metrics""" | |
| if not self.genotype_counts: | |
| return { | |
| 'unique_genotypes': 0, | |
| 'max_frequency': 0.0, | |
| 'diversity_index': 0.0, | |
| 'unique_genotypes_ratio': 0.0 | |
| } | |
| total = sum(self.genotype_counts.values()) | |
| unique = len(self.genotype_counts) | |
| max_freq = max(self.genotype_counts.values()) / total if total > 0 else 0.0 | |
| # Shannon diversity index | |
| diversity_index = 0.0 | |
| for count in self.genotype_counts.values(): | |
| if count > 0: | |
| p = count / total | |
| diversity_index -= p * math.log2(p) if p > 0 else 0 | |
| return { | |
| 'unique_genotypes': unique, | |
| 'max_frequency': max_freq, | |
| 'diversity_index': diversity_index, | |
| 'unique_genotypes_ratio': unique / total if total > 0 else 0.0 | |
| } | |
| class EvolutionEngine: | |
| """ | |
| Main evolution engine coordinating all genetic operations | |
| """ | |
| def __init__(self, | |
| population_size: int = 25, | |
| genotype_length: int = 32, | |
| max_generations: int = 1000, | |
| fitness_targets: Optional[Dict[str, float]] = None, | |
| config: Optional[Dict[str, Any]] = None): | |
| self.population_size = population_size | |
| self.genotype_length = genotype_length | |
| self.max_generations = max_generations | |
| self.config = config or {} | |
| # Get evolution config for component initialization | |
| evolution_config = self.config.get('evolution', {}) | |
| # Initialize components with config values | |
| self.selection = SelectionEngine( | |
| tournament_size=evolution_config.get('tournament_size', 5), | |
| elitism_rate=evolution_config.get('elitism_rate', 0.1) | |
| ) | |
| mutation_rate_config = evolution_config.get('mutation_rate', {}) | |
| initial_mutation_rate = mutation_rate_config.get('initial', 0.04) if isinstance(mutation_rate_config, dict) else mutation_rate_config | |
| self.mutation = MutationEngine( | |
| base_rate=initial_mutation_rate, | |
| adaptive=evolution_config.get('adaptive_mutation', True) | |
| ) | |
| self.fitness_cache = FitnessCache() | |
| # Initialize diversity guard from config | |
| diversity_config = self.config.get('evolution', {}).get('diversity_guard', {}) | |
| if diversity_config: | |
| self.diversity_guard = DiversityGuard( | |
| hash_similarity_threshold=diversity_config.get('hash_similarity_threshold', 0.92), | |
| penalty=diversity_config.get('penalty', 0.05), | |
| frequency_threshold=diversity_config.get('frequency_threshold', 0.1), | |
| enabled=diversity_config.get('enabled', True) | |
| ) | |
| else: | |
| self.diversity_guard = DiversityGuard(enabled=False) | |
| # Fitness targets for evaluation | |
| self.fitness_targets = fitness_targets or { | |
| 'trait_0': 0.8, # Target high values for some traits | |
| 'trait_1': 0.2, # Target low values for others | |
| 'trait_2': 0.5, # Target medium values | |
| } | |
| # Population tracking | |
| self.population: List[Organism] = [] | |
| self.generation = 0 | |
| self.best_fitness_history = [] | |
| self.average_fitness_history = [] | |
| # Initialize population | |
| self._initialize_population() | |
| def _initialize_population(self): | |
| """Create initial random population with loading bar""" | |
| import sys | |
| self.population = [] | |
| # Show loading bar for brain creation | |
| neural_enabled = self.config.get('neural', {}).get('enabled', False) | |
| if neural_enabled: | |
| print(f"[EVOLUTION] 🧠 Creating {self.population_size} neural brains...", end="", flush=True) | |
| for i in range(self.population_size): | |
| # Random binary genotype | |
| genes = np.random.randint(0, 2, self.genotype_length, dtype=np.uint8) | |
| genotype = Genotype(genes=genes, generation=0) | |
| organism = self._create_organism(genotype) | |
| self.population.append(organism) | |
| # Progress bar (update every 10% or every organism if small population) | |
| if neural_enabled: | |
| progress = (i + 1) / self.population_size | |
| bar_width = 30 | |
| filled = int(bar_width * progress) | |
| bar = "█" * filled + "░" * (bar_width - filled) | |
| sys.stdout.write(f"\r[EVOLUTION] 🧠 Creating brains: [{bar}] {i+1}/{self.population_size}") | |
| sys.stdout.flush() | |
| if neural_enabled: | |
| print() # Newline after progress bar | |
| print(f"[EVOLUTION] ✅ All {self.population_size} brains created successfully") | |
| def evolve_generation(self) -> Dict[str, Any]: | |
| """Run one generation of evolution with consciousness tracking""" | |
| start_time = time.time() | |
| # Evaluate fitness (with caching) | |
| self._evaluate_population() | |
| # Track statistics | |
| fitnesses = [org.fitness for org in self.population] | |
| self.best_fitness_history.append(max(fitnesses)) | |
| self.average_fitness_history.append(np.mean(fitnesses)) | |
| # Selection | |
| num_elites = int(self.population_size * self.selection.elitism_rate) | |
| elites = self.selection.elitism_selection(self.population, num_elites) | |
| num_parents = self.population_size - num_elites | |
| parents = self.selection.tournament_selection(self.population, num_parents) | |
| # Reproduction | |
| offspring = self._create_offspring(parents, num_parents) | |
| # Mutation | |
| mutation_rate = self.mutation.get_adaptive_rate(self.population, self.generation) | |
| mutated_offspring = self.mutation.apply_mutations(offspring, mutation_rate) | |
| # Update mutation stats | |
| self.mutation.update_stats(self.population) | |
| # Create new population | |
| new_population = elites.copy() | |
| for genotype in mutated_offspring: | |
| organism = self._create_organism(genotype, parents=parents) | |
| new_population.append(organism) | |
| self.population = new_population[:self.population_size] | |
| self.generation += 1 | |
| # Update diversity guard | |
| self.diversity_guard.update_generation(self.population) | |
| # Get diversity metrics | |
| diversity_metrics = self.diversity_guard.get_diversity_metrics() | |
| # Log warning if diversity is low | |
| if diversity_metrics['max_frequency'] > 0.2: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.warning(f"[Diversity] High genotype frequency: {diversity_metrics['max_frequency']:.2%}, " | |
| f"unique genotypes: {diversity_metrics['unique_genotypes']}/{len(self.population)}") | |
| # Performance metrics | |
| elapsed = time.time() - start_time | |
| return { | |
| 'generation': self.generation, | |
| 'best_fitness': max(fitnesses), | |
| 'avg_fitness': np.mean(fitnesses), | |
| 'mutation_rate': mutation_rate, | |
| 'elapsed_seconds': elapsed, | |
| 'population_diversity': np.std(fitnesses), | |
| 'unique_genotypes': diversity_metrics['unique_genotypes'], | |
| 'max_genotype_frequency': diversity_metrics['max_frequency'], | |
| 'diversity_index': diversity_metrics['diversity_index'], | |
| 'unique_genotypes_ratio': diversity_metrics['unique_genotypes_ratio'] | |
| } | |
| def _evaluate_population(self): | |
| """Evaluate fitness for all organisms (with caching, diversity penalty, and ML bonuses)""" | |
| for organism in self.population: | |
| # Check cache first | |
| genotype_hash = organism.genotype.get_hash() | |
| cached_fitness = self.fitness_cache.get(genotype_hash) | |
| if cached_fitness is not None: | |
| base_fitness = cached_fitness | |
| else: | |
| # Develop phenotype and calculate fitness | |
| organism.develop_phenotype() | |
| base_fitness = organism.calculate_fitness(self.fitness_targets) | |
| # Cache base fitness (before diversity penalty) | |
| self.fitness_cache.put(genotype_hash, base_fitness) | |
| # Apply diversity penalty | |
| penalty = self.diversity_guard.apply_diversity_penalty(organism, self.population) | |
| adjusted_fitness = base_fitness - penalty | |
| # NEW: Apply language-aware fitness bonus (if ML analysis available) | |
| language_bonus = self._calculate_language_fitness_bonus(organism, getattr(self, '_ml_analysis', None)) | |
| adjusted_fitness += language_bonus | |
| # ML-AWARE EVOLUTION: Apply cluster/anomaly/concept bonuses | |
| # This uses scikit-learn analysis to influence selection pressure | |
| organism_id = getattr(organism, 'id', str(id(organism))) | |
| ml_bonus = self.apply_ml_selection_bonus(organism, organism_id) | |
| adjusted_fitness += ml_bonus | |
| # Allow fitness to exceed 1.0 | |
| organism.fitness = max(0.0, adjusted_fitness) # Only prevent negative | |
| organism.genotype.fitness = organism.fitness | |
| def _calculate_language_fitness_bonus(self, organism: Organism, ml_analysis: Optional[Dict[str, Any]]) -> float: | |
| """ | |
| Calculate language-aware fitness bonus based on functional vocabulary. | |
| Rewards organisms with: | |
| - Functional vocabulary (words that predict fitness) | |
| - Unique vocabulary (diversity bonus) | |
| - Language-behavior alignment | |
| Returns bonus (0.0 to 0.1) to add to fitness. | |
| """ | |
| if not ml_analysis or not ml_analysis.get('enabled'): | |
| return 0.0 | |
| semantic_analysis = ml_analysis.get('semantic_analysis', {}) | |
| if not semantic_analysis: | |
| return 0.0 | |
| bonus = 0.0 | |
| # Feature importance from feature selection (words that predict fitness) | |
| feature_importance = semantic_analysis.get('feature_importance', {}) | |
| if feature_importance: | |
| # Get organism's vocabulary (would need context_memory - stored in network) | |
| # For now, give small bonus if feature selection found predictive words | |
| predictive_words = feature_importance.get('top_predictive_words', []) | |
| if len(predictive_words) > 0: | |
| # Small bonus for having a system that can identify functional words | |
| bonus += 0.02 | |
| # Quality metrics bonus | |
| quality_metrics = semantic_analysis.get('quality_metrics', {}) | |
| if quality_metrics: | |
| silhouette = quality_metrics.get('silhouette_score', 0.0) | |
| # If language clusters are well-formed, small bonus | |
| if silhouette > 0.5: | |
| bonus += 0.02 # Small bonus for good language structure | |
| return min(0.1, bonus) # Cap at 0.1 (10% fitness bonus) | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| # ML-AWARE EVOLUTION: Use scikit-learn analysis to influence selection | |
| # Groks identified that ML data wasn't being used to affect behavior | |
| # ═══════════════════════════════════════════════════════════════════════════ | |
| def apply_ml_selection_bonus(self, organism: 'Organism', organism_id: str) -> float: | |
| """ | |
| Apply fitness bonus/penalty based on ML analysis results. | |
| Uses: | |
| - Cluster membership: organisms in larger clusters get small cooperation bonus | |
| - Anomaly status: anomalies get exploration bonus (they're trying something new) | |
| - Concept stability: stable behavioral phenotypes get consistency bonus | |
| Returns bonus/penalty to add to fitness (-0.1 to +0.1 range) | |
| """ | |
| ml_analysis = getattr(self, '_ml_analysis', None) | |
| if not ml_analysis or not ml_analysis.get('enabled'): | |
| return 0.0 | |
| bonus = 0.0 | |
| # 1. CLUSTER-AWARE COOPERATION BONUS | |
| # Organisms in larger clusters (cooperating behavioral phenotypes) get bonus | |
| cluster_labels = ml_analysis.get('cluster_labels', []) | |
| organism_ids = ml_analysis.get('organism_ids', []) # FIX: organism_ids is a list, not a dict | |
| # Validate alignment - if missing or mismatched, ML bonuses can't apply | |
| if not organism_ids or len(organism_ids) != len(cluster_labels): | |
| return 0.0 # No valid mapping, no bonus | |
| if cluster_labels: | |
| if organism_id in organism_ids: | |
| idx = organism_ids.index(organism_id) | |
| if idx < len(cluster_labels): | |
| cluster_id = cluster_labels[idx] | |
| if cluster_id >= 0: # Not noise/outlier (-1) | |
| cluster_sizes = ml_analysis.get('clustering', {}).get('cluster_sizes', {}) | |
| cluster_size = cluster_sizes.get(cluster_id, 0) | |
| # Larger clusters = more organisms with similar successful behavior | |
| if cluster_size >= 5: | |
| bonus += 0.02 # Cooperation bonus | |
| elif cluster_size >= 3: | |
| bonus += 0.01 | |
| # 2. ANOMALY EXPLORATION BONUS | |
| # Anomalies are trying novel strategies - reward exploration | |
| anomaly_organisms = ml_analysis.get('anomaly_organisms', []) | |
| if organism_id in anomaly_organisms: | |
| # Small exploration bonus for trying something different | |
| bonus += 0.03 # Exploration bonus (encourages innovation) | |
| # 3. CONCEPT STABILITY BONUS | |
| # Organisms in stable behavioral phenotypes (tracked concepts) get consistency bonus | |
| concept_tags = ml_analysis.get('concept_tags', {}) | |
| if cluster_labels and concept_tags: | |
| # organism_ids already validated above, reuse it | |
| if organism_id in organism_ids: | |
| idx = organism_ids.index(organism_id) | |
| if idx < len(cluster_labels): | |
| cluster_id = cluster_labels[idx] | |
| if cluster_id in concept_tags: | |
| # This organism is part of a named, stable concept | |
| bonus += 0.02 # Stability bonus | |
| # Clamp to reasonable range | |
| return np.clip(bonus, -0.1, 0.1) | |
| def get_cluster_crossover_candidates(self, parents: List['Organism']) -> List[Tuple['Organism', 'Organism']]: | |
| """ | |
| Use ML clustering to suggest better crossover pairs. | |
| Organisms in the SAME cluster have similar successful strategies, | |
| so crossing them may produce more viable offspring. | |
| Organisms in DIFFERENT clusters bring diversity. | |
| Returns list of (parent1, parent2) pairs optimized for both. | |
| """ | |
| ml_analysis = getattr(self, '_ml_analysis', None) | |
| if not ml_analysis or not ml_analysis.get('enabled'): | |
| # Fallback to random pairing | |
| return [] | |
| cluster_labels = ml_analysis.get('cluster_labels', []) | |
| if not cluster_labels: | |
| return [] | |
| # Group parents by cluster | |
| cluster_groups: Dict[int, List['Organism']] = {} | |
| organism_ids = list(ml_analysis.get('organism_ids', {}).keys()) if 'organism_ids' in ml_analysis else [] | |
| for parent in parents: | |
| parent_id = getattr(parent, 'id', str(id(parent))) | |
| if parent_id in organism_ids: | |
| idx = organism_ids.index(parent_id) | |
| if idx < len(cluster_labels): | |
| cluster_id = cluster_labels[idx] | |
| if cluster_id not in cluster_groups: | |
| cluster_groups[cluster_id] = [] | |
| cluster_groups[cluster_id].append(parent) | |
| pairs = [] | |
| # 70% same-cluster pairs (exploit successful strategies) | |
| # 30% cross-cluster pairs (explore diversity) | |
| for cluster_id, members in cluster_groups.items(): | |
| if len(members) >= 2: | |
| # Same-cluster pairs | |
| for i in range(0, len(members) - 1, 2): | |
| if np.random.random() < 0.7: | |
| pairs.append((members[i], members[i + 1])) | |
| # Cross-cluster pairs | |
| cluster_ids = list(cluster_groups.keys()) | |
| if len(cluster_ids) >= 2: | |
| for _ in range(len(pairs) // 3): # Add 30% cross-cluster | |
| c1, c2 = np.random.choice(cluster_ids, 2, replace=False) | |
| if cluster_groups[c1] and cluster_groups[c2]: | |
| p1 = np.random.choice(cluster_groups[c1]) | |
| p2 = np.random.choice(cluster_groups[c2]) | |
| pairs.append((p1, p2)) | |
| return pairs | |
| def _create_offspring(self, parents: List[Organism], num_offspring: int) -> List[Genotype]: | |
| """Create offspring from selected parents""" | |
| offspring = [] | |
| while len(offspring) < num_offspring: | |
| # Select two random parents | |
| parent1, parent2 = np.random.choice(parents, 2, replace=False) | |
| # Crossover | |
| child1_genotype, child2_genotype = parent1.genotype.crossover(parent2.genotype) | |
| offspring.extend([child1_genotype, child2_genotype]) | |
| return offspring[:num_offspring] | |
| def evolve_for_generations(self, num_generations: int = 100, | |
| progress_callback: Optional[Callable] = None) -> Dict[str, List]: | |
| """Run evolution for multiple generations""" | |
| history = { | |
| 'generations': [], | |
| 'best_fitness': [], | |
| 'avg_fitness': [], | |
| 'mutation_rates': [], | |
| 'elapsed_times': [], | |
| 'diversity': [] | |
| } | |
| for gen in range(num_generations): | |
| if gen >= self.max_generations: | |
| break | |
| stats = self.evolve_generation() | |
| # Record history with correct key mapping | |
| history['generations'].append(stats.get('generation', 0)) | |
| history['best_fitness'].append(stats.get('best_fitness', 0.0)) | |
| history['avg_fitness'].append(stats.get('avg_fitness', 0.0)) | |
| history['mutation_rates'].append(stats.get('mutation_rate', 0.0)) | |
| history['elapsed_times'].append(stats.get('elapsed_seconds', 0.0)) | |
| history['diversity'].append(stats.get('population_diversity', 0.0)) | |
| # Progress callback | |
| if progress_callback: | |
| progress_callback(stats) | |
| return history | |
| def get_population_stats(self) -> Dict[str, Any]: | |
| """Get comprehensive population statistics""" | |
| if not self.population: | |
| return {} | |
| fitnesses = [org.fitness for org in self.population] | |
| genotypes = [org.genotype for org in self.population] | |
| # Get diversity metrics | |
| diversity_metrics = self.diversity_guard.get_diversity_metrics() | |
| return { | |
| 'population_size': len(self.population), | |
| 'generation': self.generation, | |
| 'best_fitness': max(fitnesses), | |
| 'avg_fitness': np.mean(fitnesses), | |
| 'fitness_std': np.std(fitnesses), | |
| 'genotype_diversity': len(set(g.get_hash() for g in genotypes)), | |
| 'cache_stats': self.fitness_cache.stats(), | |
| 'mutation_stats': self.mutation.generation_stats, | |
| 'diversity_guard': { | |
| 'enabled': self.diversity_guard.enabled, | |
| 'unique_genotypes': diversity_metrics['unique_genotypes'], | |
| 'max_genotype_frequency': diversity_metrics['max_frequency'], | |
| 'diversity_index': diversity_metrics['diversity_index'], | |
| 'unique_genotypes_ratio': diversity_metrics['unique_genotypes_ratio'] | |
| } | |
| } | |
| def get_best_organism(self) -> Optional[Organism]: | |
| """Get the organism with highest fitness""" | |
| if not self.population: | |
| return None | |
| return max(self.population, key=lambda org: org.fitness) | |
| def set_mutation_rate(self, rate: float): | |
| """Set the base mutation rate for the mutation engine""" | |
| self.mutation.base_rate = max(0.001, min(0.1, rate)) # Clamp to reasonable bounds | |
| def get_mutation_rate(self) -> float: | |
| """Get the current base mutation rate""" | |
| return self.mutation.base_rate | |
| def sync_from_atomic_config(self, atomic_config_system) -> Dict[str, Any]: | |
| """ | |
| INTEGRATION FIX: Sync tunable parameters from AtomicConfigSystem. | |
| This bridges the gap where atoms are tuned but evolution engine doesn't see changes. | |
| Call this periodically (e.g., every generation) to pull updated values. | |
| Args: | |
| atomic_config_system: AtomicConfigSystem instance | |
| Returns: | |
| Dict of parameters that were updated | |
| """ | |
| if atomic_config_system is None: | |
| return {} | |
| updated = {} | |
| # Mutation rate | |
| new_rate = atomic_config_system.get('mutation_rate') | |
| if new_rate is not None and new_rate != self.mutation.base_rate: | |
| old_rate = self.mutation.base_rate | |
| self.set_mutation_rate(new_rate) | |
| updated['mutation_rate'] = {'old': old_rate, 'new': self.mutation.base_rate} | |
| # Elitism rate | |
| new_elitism = atomic_config_system.get('elitism_rate') | |
| if new_elitism is not None and new_elitism != self.selection.elitism_rate: | |
| old_elitism = self.selection.elitism_rate | |
| self.selection.elitism_rate = max(0.0, min(0.5, new_elitism)) # Clamp | |
| updated['elitism_rate'] = {'old': old_elitism, 'new': self.selection.elitism_rate} | |
| # Tournament size | |
| new_tournament = atomic_config_system.get('tournament_size') | |
| if new_tournament is not None and new_tournament != self.selection.tournament_size: | |
| old_tournament = self.selection.tournament_size | |
| self.selection.tournament_size = max(2, min(20, int(new_tournament))) # Clamp | |
| updated['tournament_size'] = {'old': old_tournament, 'new': self.selection.tournament_size} | |
| # Log if anything changed | |
| if updated: | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| logger.info(f"[EVOLUTION] Synced {len(updated)} params from AtomicConfigSystem: {list(updated.keys())}") | |
| return updated | |
| def _create_organism(self, genotype: Genotype, parents: Optional[List[Organism]] = None) -> Organism: | |
| """ | |
| Factory method for organism creation. | |
| Creates NeuralOrganism if neural is enabled, otherwise standard Organism. | |
| Args: | |
| genotype: Organism genotype | |
| parents: Parent organisms (for brain inheritance) | |
| Returns: | |
| Organism or NeuralOrganism instance | |
| """ | |
| neural_config = self.config.get('neural', {}) | |
| if neural_config.get('enabled', False): | |
| try: | |
| from .neural.neural_organism import NeuralOrganism | |
| # Try to inherit brains from parents if available | |
| parent_brains = [] | |
| if parents and len(parents) > 0: | |
| # Collect brains from neural parents (up to 2) | |
| for parent in parents: | |
| if hasattr(parent, 'brain') and parent.brain is not None: | |
| parent_brains.append(parent.brain) | |
| if len(parent_brains) >= 2: | |
| break | |
| organism = NeuralOrganism( | |
| genotype=genotype, | |
| config=self.config, | |
| parent_brains=parent_brains if parent_brains else None | |
| ) | |
| except ImportError: | |
| # PyTorch not available, fall back to standard Organism | |
| organism = Organism(genotype=genotype) | |
| else: | |
| organism = Organism(genotype=genotype) | |
| # 🧬 CRITICAL: Develop phenotype immediately after creation | |
| # This expresses genetic traits including curiosity (gene 10) | |
| # Without this, curiosity and other traits remain at default values! | |
| organism.develop_phenotype() | |
| # ✅ FIX: Calculate initial fitness from genotype diversity | |
| # This creates differentiation from birth, preventing convergence | |
| initial_fitness = self._calculate_initial_fitness(genotype, parents) | |
| organism.fitness = initial_fitness | |
| if hasattr(organism, 'genotype'): | |
| organism.genotype.fitness = initial_fitness | |
| return organism | |
| def _calculate_initial_fitness(self, genotype: Genotype, parents: Optional[List[Organism]] = None) -> float: | |
| """ | |
| Calculate initial fitness based on genetic diversity and parent fitness. | |
| Creates meaningful differentiation from organism creation. | |
| Prevents all organisms from starting at fitness = 0.0. | |
| Args: | |
| genotype: Organism genotype | |
| parents: Parent organisms (if any) | |
| Returns: | |
| Initial fitness value (0.0-1.0) | |
| """ | |
| # Base fitness from genetic diversity (more diverse = slightly higher) | |
| gene_variance = np.var(genotype.genes) if len(genotype.genes) > 0 else 0.0 | |
| diversity_bonus = min(gene_variance / 10000.0, 0.1) # Max 0.1 bonus for high diversity | |
| # Parent fitness inheritance (if parents exist) | |
| parent_fitness_bonus = 0.0 | |
| if parents and len(parents) > 0: | |
| parent_fitnesses = [p.fitness for p in parents if hasattr(p, 'fitness')] | |
| if parent_fitnesses: | |
| avg_parent_fitness = np.mean(parent_fitnesses) | |
| # Inherit 30% of parent fitness (Lamarckian inheritance) | |
| parent_fitness_bonus = avg_parent_fitness * 0.3 | |
| # Genetic uniqueness bonus (hash-based, ensures different organisms get different values) | |
| genotype_hash = genotype.get_hash() | |
| # Use hash to create deterministic but varied initial fitness | |
| try: | |
| hash_int = int(genotype_hash[:8], 16) if len(genotype_hash) >= 8 else hash(genotype_hash) | |
| except (ValueError, TypeError): | |
| hash_int = hash(genotype_hash) % 1000000 | |
| uniqueness_bonus = (hash_int % 1000) / 10000.0 # 0.0-0.1 range | |
| # Base fitness: 0.3-0.5 range (prevents starting at 0.0) | |
| base_fitness = 0.3 + uniqueness_bonus | |
| # Total initial fitness | |
| initial_fitness = base_fitness + diversity_bonus + parent_fitness_bonus | |
| # Allow any positive initial fitness | |
| return max(0.0, initial_fitness) | |
| # Utility functions for easy use | |
| def create_evolution_engine(population_size: int = 25, | |
| genotype_length: int = 32, | |
| fitness_targets: Optional[Dict[str, float]] = None, | |
| config: Optional[Dict[str, Any]] = None) -> EvolutionEngine: | |
| """Create a ready-to-use evolution engine""" | |
| return EvolutionEngine( | |
| population_size=population_size, | |
| genotype_length=genotype_length, | |
| fitness_targets=fitness_targets, | |
| config=config | |
| ) | |
| def simple_fitness_function(organism: Organism) -> float: | |
| """Simple fitness function for testing""" | |
| # Reward organisms with balanced traits | |
| total_balance = 0.0 | |
| for trait_name, trait_value in organism.phenotype.traits.items(): | |
| # Ideal trait values depend on trait name | |
| if 'trait_0' in trait_name: | |
| ideal = 0.8 | |
| elif 'trait_1' in trait_name: | |
| ideal = 0.2 | |
| else: | |
| ideal = 0.5 | |
| balance = 1.0 - abs(trait_value - ideal) | |
| total_balance += balance | |
| return total_balance / max(1, len(organism.phenotype.traits)) | |
| # Module-level docstring | |
| """ | |
| 🧬 EVOLUTION ENGINE = GENETIC ALGORITHMS FOR LIFE | |
| This module brings Darwinian evolution to the simulator: | |
| - Organisms with heritable traits compete and reproduce | |
| - Fitness evaluation drives natural selection | |
| - Mutations introduce variation | |
| - Caching prevents redundant calculations | |
| - Adaptive rates respond to evolutionary pressure | |
| Where quantum particles become living, evolving organisms. | |
| """ | |
Xet Storage Details
- Size:
- 50.8 kB
- Xet hash:
- 06452c406933e0ddb3f48bf021673842b0f004daf612eb900cc20bf961825b8a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.