tostido's picture
download
raw
50.8 kB
"""
🧬 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
@dataclass
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
@dataclass
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)
@dataclass
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.