Evolution Process

This document explains how Cadence evolves programs using Large Language Models as mutation operators.

Overview

Cadence implements an evolutionary algorithm where: - Population: Collection of program variants - Parents: Selected programs for generating offspring - Mutation: LLM-generated code modifications - Selection: Performance-based survival of programs - Fitness: Objective function evaluation

Evolution Loop

The core evolution process follows these steps:

1. Initialization

# Create initial population
population = []
for i in range(population_size):
    program = task.baseline_program
    cost = evaluate_program(program)
    population.append({
        "code": program,
        "cost": cost,
        "generation": 0,
        "id": i
    })

2. Parent Selection

Programs are selected for mutation based on performance:

def select_parent(population, method="tournament"):
    if method == "tournament":
        # Tournament selection
        candidates = random.sample(population, k=3)
        return min(candidates, key=lambda p: p["cost"])

    elif method == "roulette":
        # Fitness-proportionate selection
        weights = [1.0 / (1.0 + p["cost"]) for p in population]
        return random.choices(population, weights=weights)[0]

3. Prompt Generation

Create evolution prompt for the LLM:

def generate_evolution_prompt(parent, children, instructions):
    prompt = f"""
Task: Improve the following program for {task.function_name}

Parent Program:
```python
{parent["code"]}

Previous Children (for reference): {format_children(children)}

Instructions: {instructions}

Generate improved code for the marked blocks. """ return prompt


### 4. LLM Mutation

The LLM generates code modifications:

```python
def mutate_program(parent_code, llm, prompt):
    # Get LLM response
    response = llm.generate(prompt)

    # Extract code diffs
    diffs = parse_code_blocks(response)

    # Apply diffs to parent
    child_code = apply_diff(parent_code, diffs)

    return child_code

5. Evaluation

Assess the generated program:

def evaluate_program(code, task, num_seeds=5):
    results = []

    for seed in range(num_seeds):
        try:
            # Generate test input
            input_data = task.generate_inputs(seed)

            # Execute program
            func = extract_function(code, task.function_name)
            output = func(input_data)

            # Evaluate result
            result = task.evaluate(output, input_data)
            results.append(result)

        except Exception as e:
            results.append({
                "cost": float("inf"),
                "feasible": False,
                "error": str(e)
            })

    # Aggregate results
    avg_cost = sum(r["cost"] for r in results) / len(results)
    feasible = all(r["feasible"] for r in results)

    return {
        "cost": avg_cost,
        "feasible": feasible,
        "individual_results": results
    }

6. Population Update

Integrate successful mutations:

def update_population(population, new_program, generation):
    # Add new program
    new_program["generation"] = generation
    population.append(new_program)

    # Remove worst performers if population too large
    if len(population) > max_population_size:
        population.sort(key=lambda p: p["cost"])
        population = population[:max_population_size]

    return population

Selection Strategies

Tournament Selection

def tournament_selection(population, tournament_size=3):
    """Select parent via tournament selection."""
    candidates = random.sample(population, k=tournament_size)
    return min(candidates, key=lambda p: p["cost"])

Elitism

def elitist_selection(population, elite_size=2):
    """Preserve best performers across generations."""
    population.sort(key=lambda p: p["cost"])
    return population[:elite_size]

Diversity-Based Selection

def diversity_selection(population, parent):
    """Select diverse parents to avoid local optima."""
    distances = []
    for candidate in population:
        # Calculate code similarity
        similarity = compute_code_similarity(parent["code"], candidate["code"])
        distances.append((candidate, 1.0 - similarity))

    # Select based on diversity and performance
    weights = [d[1] * (1.0 / (1.0 + d[0]["cost"])) for d in distances]
    return random.choices([d[0] for d in distances], weights=weights)[0]

Mutation Strategies

Block-Based Mutation

The primary mutation strategy targets marked code blocks:

# Original code
def tsp(cities):
    ### START_BLOCK
    n = len(cities)
    return list(range(n))
    ### END_BLOCK

# After mutation
def tsp(cities):
    ### START_BLOCK
    n = len(cities)
    best_tour = None
    best_dist = float('inf')

    for start in range(n):
        tour = nearest_neighbor(cities, start)
        dist = calculate_total_distance(tour, cities)
        if dist < best_dist:
            best_tour = tour
            best_dist = dist

    return best_tour
    ### END_BLOCK

Instruction Evolution

Instructions to the LLM can also evolve:

def evolve_instructions(current_instructions, generation, best_cost):
    if generation % 10 == 0:  # Every 10 generations
        meta_prompt = f"""
Current instructions: {current_instructions}
Best cost achieved: {best_cost}
Generation: {generation}

Suggest improved instructions for evolving TSP solutions.
Focus on: algorithmic improvements, efficiency, edge cases.
"""
        new_instructions = llm.generate(meta_prompt)
        return new_instructions
    return current_instructions

Fitness Evaluation

Multi-Objective Fitness

def calculate_fitness(program, task):
    # Primary objective
    cost = evaluate_program_cost(program, task)

    # Secondary objectives
    complexity = calculate_code_complexity(program["code"])
    execution_time = measure_execution_time(program, task)

    # Weighted combination
    fitness = {
        "cost": cost,
        "complexity": complexity,
        "execution_time": execution_time,
        "combined": 0.7 * cost + 0.2 * complexity + 0.1 * execution_time
    }

    return fitness

Constraint Handling

def evaluate_with_constraints(program, task):
    result = task.evaluate(program["output"], program["input"])

    # Apply constraint penalties
    if not result["feasible"]:
        result["cost"] = float("inf")

    # Time constraint
    if program["execution_time"] > MAX_EXECUTION_TIME:
        result["cost"] *= 1.5  # Penalty for slow solutions

    # Complexity constraint
    if calculate_complexity(program["code"]) > MAX_COMPLEXITY:
        result["cost"] *= 1.2  # Penalty for complex solutions

    return result

Advanced Techniques

Island Model

Run multiple parallel populations:

def island_evolution(num_islands=4, migration_rate=0.1):
    islands = [initialize_population() for _ in range(num_islands)]

    for generation in range(max_generations):
        # Evolve each island
        for island in islands:
            evolve_generation(island)

        # Migration between islands
        if generation % 10 == 0:
            migrate_individuals(islands, migration_rate)

    # Combine final populations
    final_population = []
    for island in islands:
        final_population.extend(island)

    return final_population

Adaptive Parameters

Adjust evolution parameters based on progress:

def adaptive_parameters(generation, stagnation_count):
    # Increase mutation rate if stagnating
    if stagnation_count > 5:
        mutation_rate = min(0.9, base_mutation_rate * 1.5)
    else:
        mutation_rate = base_mutation_rate

    # Adjust population size
    if generation < 20:
        population_size = 20  # Explore widely early
    else:
        population_size = 10  # Focus on best later

    return {
        "mutation_rate": mutation_rate,
        "population_size": population_size
    }

Encourage exploration of diverse solutions:

def novelty_score(program, archive):
    distances = []
    for archived_program in archive:
        # Calculate behavioral distance
        behavior_dist = calculate_behavior_distance(
            program["behavior"],
            archived_program["behavior"]
        )
        distances.append(behavior_dist)

    # Novelty = average distance to k nearest neighbors
    distances.sort()
    k = min(5, len(distances))
    novelty = sum(distances[:k]) / k

    return novelty

def behavior_distance(behavior1, behavior2):
    # Compare program behaviors across test cases
    return sum(abs(b1 - b2) for b1, b2 in zip(behavior1, behavior2))

Monitoring Evolution

Progress Tracking

def track_evolution_progress(population, generation):
    best_cost = min(p["cost"] for p in population)
    avg_cost = sum(p["cost"] for p in population) / len(population)
    diversity = calculate_population_diversity(population)

    metrics = {
        "generation": generation,
        "best_cost": best_cost,
        "average_cost": avg_cost,
        "diversity": diversity,
        "population_size": len(population)
    }

    log_metrics(metrics)
    return metrics

Convergence Detection

def check_convergence(history, patience=10, tolerance=1e-6):
    if len(history) < patience:
        return False

    recent_best = [h["best_cost"] for h in history[-patience:]]
    improvement = recent_best[0] - recent_best[-1]

    return improvement < tolerance

Configuration

Evolution Parameters

EVOLUTION_CONFIG = {
    "population_size": 20,
    "generations": 100,
    "tournament_size": 3,
    "elite_size": 2,
    "mutation_rate": 0.8,
    "migration_rate": 0.1,
    "stagnation_threshold": 10
}

LLM Parameters

LLM_CONFIG = {
    "temperature": 0.7,
    "max_tokens": 2048,
    "top_p": 0.9,
    "frequency_penalty": 0.1
}