diff --git a/.gitignore b/.gitignore index 9b46e311f..4ca4d023e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ ENV/ # Output files examples/*/output/ -openevolve_output/ +openevolve_output*/ *.log # Test cache diff --git a/examples/circle_packing/README.md b/examples/circle_packing/README.md new file mode 100644 index 000000000..878b666f1 --- /dev/null +++ b/examples/circle_packing/README.md @@ -0,0 +1,240 @@ +# Circle Packing Example + +This example demonstrates how OpenEvolve can be used to tackle the challenging mathematical problem of circle packing, a classic problem in computational geometry. Specifically, we focus on packing 26 circles of varying sizes into a unit square to maximize the sum of their radii, replicating one of the tasks from the AlphaEvolve paper. + +## Problem Overview + +The circle packing problem involves placing n non-overlapping circles inside a container (in this case, a unit square) to optimize a specific metric. For this example: + +- We pack exactly 26 circles +- Each circle must lie entirely within the unit square +- No circles may overlap +- We aim to maximize the sum of all circle radii + +According to the AlphaEvolve paper, a solution with a sum of radii of approximately 2.635 is achievable for n=26. Our goal was to match or exceed this result. + +## Our Approach + +We structured our evolution in two phases, each with a different configuration to encourage exploration and exploitation at different stages: + +### Phase 1: Initial Exploration + +In the first phase, we focused on exploring different fundamental approaches to the packing problem: + +- Used a constructor-based approach that places circles in strategic positions +- Explored various geometric patterns (concentric rings, grid-based arrangements, etc.) +- Developed simple optimization routines to maximize circle sizes without overlaps + +Configuration highlights: +```yaml +max_iterations: 100 +population_size: 60 +num_islands: 4 +exploitation_ratio: 0.7 +``` + +### Phase 2: Breaking Through the Plateau + +After the initial exploration phase, we observed our solutions plateauing around 2.377. For the second phase, we reconfigured OpenEvolve to encourage more radical innovations: + +- Increased the population size to promote diversity +- Lowered the exploitation ratio to favor exploration +- Updated the system prompt to suggest different optimization techniques +- Allowed for longer and more complex code solutions + +Configuration highlights: +```yaml +max_iterations: 100 +population_size: 70 +num_islands: 5 +exploitation_ratio: 0.6 +``` + +## Evolution Progress + +We tracked the evolution over 470 generations, capturing visualizations at each checkpoint. The progression shows dramatic improvements in the packing strategy: + +### Initial Solution (Generation 0) + +The initial program used a simple constructive approach with a central circle and two concentric rings: + +```python +# Initial attempt +# Place a large circle in the center +centers[0] = [0.5, 0.5] + +# Place 8 circles around it in a ring +for i in range(8): + angle = 2 * np.pi * i / 8 + centers[i + 1] = [0.5 + 0.3 * np.cos(angle), 0.5 + 0.3 * np.sin(angle)] + +# Place 16 more circles in an outer ring +for i in range(16): + angle = 2 * np.pi * i / 16 + centers[i + 9] = [0.5 + 0.7 * np.cos(angle), 0.5 + 0.7 * np.sin(angle)] +``` + +This approach yielded a sum of radii of approximately 0.959. + +![Initial Circle Packing](circle_packing_1.png) + +### Generation 10 Breakthrough + +By generation 10, OpenEvolve had already discovered a more sophisticated approach: + +```python +# Generation 10 +# Parameters for the arrangement (fine-tuned) +r_center = 0.1675 # Central circle radius + +# 1. Place central circle +centers[0] = [0.5, 0.5] +radii[0] = r_center + +# 2. First ring: 6 circles in hexagonal arrangement +r_ring1 = 0.1035 +ring1_distance = r_center + r_ring1 + 0.0005 # Small gap for stability +for i in range(6): + angle = 2 * np.pi * i / 6 + centers[i+1] = [ + 0.5 + ring1_distance * np.cos(angle), + 0.5 + ring1_distance * np.sin(angle) + ] + radii[i+1] = r_ring1 +``` + +The key innovations at this stage included: +- A carefully tuned hexagonal arrangement for the first ring +- Strategic placement of corner circles +- An additional optimization step to maximize each circle's radius + +This approach achieved a sum of radii of approximately 1.795. + +![Generation 10 Packing](circle_packing_10.png) + +### Generation 100: Grid-Based Approach + +By generation 100, OpenEvolve had pivoted to a grid-based approach with variable sized circles: + +```python +# Generation 100 +# Row 1: 5 circles +centers[0] = [0.166, 0.166] +centers[1] = [0.333, 0.166] +centers[2] = [0.500, 0.166] +centers[3] = [0.667, 0.166] +centers[4] = [0.834, 0.166] + +# Row 2: 6 circles (staggered) +centers[5] = [0.100, 0.333] +centers[6] = [0.266, 0.333] +# ... additional circles +``` + +Key innovations: +- Grid-like pattern with staggered rows +- Variable circle sizes based on position (larger in the center) +- More aggressive optimization routine with 50 iterations + +This approach achieved a sum of radii of approximately 2.201. + +![Generation 100 Packing](circle_packing_190.png) + +### Final Solution: Mathematical Optimization + +The breakthrough came when OpenEvolve discovered the power of mathematical optimization techniques. The final solution uses: + +```python +# Final solution with scipy.optimize +def construct_packing(): + # ... initialization code ... + + # Objective function: Negative sum of radii (to maximize) + def objective(x): + centers = x[:2*n].reshape(n, 2) + radii = x[2*n:] + return -np.sum(radii) + + # Constraint: No overlaps and circles stay within the unit square + def constraint(x): + centers = x[:2*n].reshape(n, 2) + radii = x[2*n:] + + # Overlap constraint + overlap_constraints = [] + for i in range(n): + for j in range(i + 1, n): + dist = np.sqrt(np.sum((centers[i] - centers[j])**2)) + overlap_constraints.append(dist - (radii[i] + radii[j])) + # ... boundary constraints ... + + # Optimization using SLSQP + result = minimize(objective, x0, method='SLSQP', bounds=bounds, constraints=constraints) +``` + +The key innovation in the final solution: +- Using `scipy.optimize.minimize` with SLSQP method to find the optimal configuration +- Formulating circle packing as a constrained optimization problem +- Representing both circle positions and radii as optimization variables +- Carefully crafted constraints to enforce non-overlap and boundary conditions + +This approach achieved a sum of radii of 2.634, matching the AlphaEvolve paper's result of 2.635 to within 0.04%! + +![Final Packing Solution](circle_packing_460.png) + +## Results + +Our final solution achieves: + +``` +Sum of radii: 2.634292402141039 +Target ratio: 0.9997314619131079 (99.97% of AlphaEvolve's result) +``` + +This demonstrates that OpenEvolve can successfully reproduce the results from the AlphaEvolve paper on this mathematical optimization problem. + +## Key Observations + +The evolution process demonstrated several interesting patterns: + +1. **Algorithm Transition**: OpenEvolve discovered increasingly sophisticated algorithms, from basic geometric constructions to advanced mathematical optimization techniques. + +2. **Exploration-Exploitation Balance**: The two-phase approach was crucial - initial exploration of different patterns followed by exploitation and refinement of the most promising approaches. + +3. **Breakthrough Discoveries**: The most significant improvements came from fundamental changes in approach (e.g., switching from manual construction to mathematical optimization), not just parameter tuning. + +4. **Code Complexity Evolution**: As the solutions improved, the code grew in complexity, adopting more sophisticated mathematical techniques. + +## Running the Example + +To reproduce our results: + +```bash +# Phase 1: Initial exploration +python openevolve-run.py examples/circle_packing/initial_program.py \ + examples/circle_packing/evaluator.py \ + --config examples/circle_packing/config_phase_1.yaml \ + --iterations 100 + +# Phase 2: Breaking through the plateau +python openevolve-run.py examples/circle_packing/openevolve_output/checkpoints/checkpoint_100/best_program.py \ + examples/circle_packing/evaluator.py \ + --config examples/circle_packing/config_phase_2.yaml \ + --iterations 100 +``` + +To visualize the best solution: + +```python +from examples.circle_packing.openevolve_output.best.best_program import run_packing, visualize + +centers, radii, sum_radii = run_packing() +print(f"Sum of radii: {sum_radii}") +visualize(centers, radii) +``` + +## Conclusion + +This example demonstrates OpenEvolve's ability to discover sophisticated algorithms for mathematical optimization problems. By evolving from simple constructive approaches to advanced numerical optimization techniques, OpenEvolve was able to match the results reported in the AlphaEvolve paper. + +The circle packing problem shows how OpenEvolve can discover not just improvements to existing algorithms, but entirely new algorithmic approaches, transitioning from manual geometric construction to principled mathematical optimization. \ No newline at end of file diff --git a/examples/circle_packing/circle_packing_1.png b/examples/circle_packing/circle_packing_1.png new file mode 100644 index 000000000..182d78fff Binary files /dev/null and b/examples/circle_packing/circle_packing_1.png differ diff --git a/examples/circle_packing/circle_packing_10.png b/examples/circle_packing/circle_packing_10.png new file mode 100644 index 000000000..4ac705ee2 Binary files /dev/null and b/examples/circle_packing/circle_packing_10.png differ diff --git a/examples/circle_packing/circle_packing_190.png b/examples/circle_packing/circle_packing_190.png new file mode 100644 index 000000000..f98434991 Binary files /dev/null and b/examples/circle_packing/circle_packing_190.png differ diff --git a/examples/circle_packing/circle_packing_460.png b/examples/circle_packing/circle_packing_460.png new file mode 100644 index 000000000..cfbfc6b24 Binary files /dev/null and b/examples/circle_packing/circle_packing_460.png differ diff --git a/examples/circle_packing/config_phase_1.yaml b/examples/circle_packing/config_phase_1.yaml new file mode 100644 index 000000000..96f1b75e5 --- /dev/null +++ b/examples/circle_packing/config_phase_1.yaml @@ -0,0 +1,56 @@ +# Configuration for circle packing constructor evolution (n=26) +max_iterations: 100 # Increased iterations +checkpoint_interval: 10 +log_level: "INFO" + +# LLM configuration +llm: + primary_model: "google/gemini-2.0-flash-001" + # primary_model: "llama3.1-8b" + primary_model_weight: 0.8 + secondary_model: "anthropic/claude-3.7-sonnet" + # secondary_model: "llama-4-scout-17b-16e-instruct" + secondary_model_weight: 0.2 + api_base: "https://openrouter.ai/api/v1" + # api_base: "https://api.cerebras.ai/v1" + temperature: 0.7 + top_p: 0.95 + max_tokens: 8192 + timeout: 600 + +# Prompt configuration +prompt: + system_message: | + You are an expert mathematician specializing in circle packing problems and computational geometry. Your task is to improve a constructor function that directly produces a specific arrangement of 26 circles in a unit square, maximizing the sum of their radii. The AlphaEvolve paper achieved a sum of 2.635 for n=26. + + Key geometric insights: + - Circle packings often follow hexagonal patterns in the densest regions + - Maximum density for infinite circle packing is pi/(2*sqrt(3)) ≈ 0.9069 + - Edge effects make square container packing harder than infinite packing + - Circles can be placed in layers or shells when confined to a square + - Similar radius circles often form regular patterns, while varied radii allow better space utilization + - Perfect symmetry may not yield the optimal packing due to edge effects + + Focus on designing an explicit constructor that places each circle in a specific position, rather than an iterative search algorithm. + num_top_programs: 3 + use_template_stochasticity: true + +# Database configuration +database: + population_size: 60 # Increased population for more diversity + archive_size: 25 + num_islands: 4 + elite_selection_ratio: 0.3 + exploitation_ratio: 0.7 + +# Evaluator configuration +evaluator: + timeout: 60 + cascade_evaluation: true + cascade_thresholds: [0.5, 0.75] + parallel_evaluations: 4 + use_llm_feedback: false + +# Evolution settings +diff_based_evolution: false # Use full rewrites instead of diffs +allow_full_rewrites: true # Allow full rewrites for constructor functions diff --git a/examples/circle_packing/config_phase_2.yaml b/examples/circle_packing/config_phase_2.yaml new file mode 100644 index 000000000..195c7d399 --- /dev/null +++ b/examples/circle_packing/config_phase_2.yaml @@ -0,0 +1,58 @@ +# Configuration for breaking through the circle packing plateau +max_iterations: 100 +checkpoint_interval: 10 +log_level: "INFO" + +# LLM configuration +llm: + primary_model: "google/gemini-2.0-flash-001" + # primary_model: "llama3.1-8b" + primary_model_weight: 0.8 + secondary_model: "anthropic/claude-3.7-sonnet" + # secondary_model: "llama-4-scout-17b-16e-instruct" + secondary_model_weight: 0.2 + api_base: "https://openrouter.ai/api/v1" + # api_base: "https://api.cerebras.ai/v1" + temperature: 0.7 + top_p: 0.95 + max_tokens: 8192 + timeout: 600 + +# Prompt configuration +prompt: + system_message: | + You are an expert mathematician specializing in circle packing problems and computational geometry. We're trying to reach the AlphaEvolve target of 2.635 for the sum of radii when packing 26 circles in a unit square. The current implementation has plateaued at 2.377, so we need significant improvements. + + Key insights to explore: + 1. The optimal arrangement likely involves variable-sized circles + 2. A pure hexagonal arrangement may not be optimal due to edge effects + 3. The densest known circle packings often use a hybrid approach + 4. The optimization routine is critically important - simple physics-based models with carefully tuned parameters + 5. Consider strategic placement of circles at square corners and edges + 6. Adjusting the pattern to place larger circles at the center and smaller at the edges + 7. The math literature suggests special arrangements for specific values of n + + Focus on breaking through the plateau by trying fundamentally different approaches - don't just tweak parameters. + num_top_programs: 4 + use_template_stochasticity: true + +# Database configuration +database: + population_size: 70 # Larger population for more diversity + archive_size: 30 + num_islands: 5 + elite_selection_ratio: 0.3 + exploitation_ratio: 0.6 # Slightly lower to encourage exploration + +# Evaluator configuration +evaluator: + timeout: 90 # Extended timeout to allow for more complex optimization + cascade_evaluation: true + cascade_thresholds: [0.5, 0.8] + parallel_evaluations: 4 + use_llm_feedback: false + +# Evolution settings +diff_based_evolution: false +allow_full_rewrites: true # Definitely allow full rewrites +max_code_length: 100000 \ No newline at end of file diff --git a/examples/circle_packing/evaluator.py b/examples/circle_packing/evaluator.py new file mode 100644 index 000000000..40669bcf6 --- /dev/null +++ b/examples/circle_packing/evaluator.py @@ -0,0 +1,311 @@ +""" +Evaluator for circle packing example (n=26) with improved timeout handling +""" + +import importlib.util +import numpy as np +import time +import os +import signal +import subprocess +import tempfile +import traceback +import sys +import pickle + + +class TimeoutError(Exception): + pass + + +def timeout_handler(signum, frame): + """Handle timeout signal""" + raise TimeoutError("Function execution timed out") + + +def validate_packing(centers, radii): + """ + Validate that circles don't overlap and are inside the unit square + + Args: + centers: np.array of shape (n, 2) with (x, y) coordinates + radii: np.array of shape (n) with radius of each circle + + Returns: + True if valid, False otherwise + """ + n = centers.shape[0] + + # Check if circles are inside the unit square + for i in range(n): + x, y = centers[i] + r = radii[i] + if x - r < -1e-6 or x + r > 1 + 1e-6 or y - r < -1e-6 or y + r > 1 + 1e-6: + print(f"Circle {i} at ({x}, {y}) with radius {r} is outside the unit square") + return False + + # Check for overlaps + for i in range(n): + for j in range(i + 1, n): + dist = np.sqrt(np.sum((centers[i] - centers[j]) ** 2)) + if dist < radii[i] + radii[j] - 1e-6: # Allow for tiny numerical errors + print(f"Circles {i} and {j} overlap: dist={dist}, r1+r2={radii[i]+radii[j]}") + return False + + return True + + +def run_with_timeout(program_path, timeout_seconds=20): + """ + Run the program in a separate process with timeout + using a simple subprocess approach + + Args: + program_path: Path to the program file + timeout_seconds: Maximum execution time in seconds + + Returns: + centers, radii, sum_radii tuple from the program + """ + # Create a temporary file to execute + with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as temp_file: + # Write a script that executes the program and saves results + script = f""" +import sys +import numpy as np +import os +import pickle +import traceback + +# Add the directory to sys.path +sys.path.insert(0, os.path.dirname('{program_path}')) + +# Debugging info +print(f"Running in subprocess, Python version: {{sys.version}}") +print(f"Program path: {program_path}") + +try: + # Import the program + spec = __import__('importlib.util').util.spec_from_file_location("program", '{program_path}') + program = __import__('importlib.util').util.module_from_spec(spec) + spec.loader.exec_module(program) + + # Run the packing function + print("Calling run_packing()...") + centers, radii, sum_radii = program.run_packing() + print(f"run_packing() returned successfully: sum_radii = {{sum_radii}}") + + # Save results to a file + results = {{ + 'centers': centers, + 'radii': radii, + 'sum_radii': sum_radii + }} + + with open('{temp_file.name}.results', 'wb') as f: + pickle.dump(results, f) + print(f"Results saved to {temp_file.name}.results") + +except Exception as e: + # If an error occurs, save the error instead + print(f"Error in subprocess: {{str(e)}}") + traceback.print_exc() + with open('{temp_file.name}.results', 'wb') as f: + pickle.dump({{'error': str(e)}}, f) + print(f"Error saved to {temp_file.name}.results") +""" + temp_file.write(script.encode()) + temp_file_path = temp_file.name + + results_path = f"{temp_file_path}.results" + + try: + # Run the script with timeout + process = subprocess.Popen( + [sys.executable, temp_file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + try: + stdout, stderr = process.communicate(timeout=timeout_seconds) + exit_code = process.returncode + + # Always print output for debugging purposes + print(f"Subprocess stdout: {stdout.decode()}") + if stderr: + print(f"Subprocess stderr: {stderr.decode()}") + + # Still raise an error for non-zero exit codes, but only after printing the output + if exit_code != 0: + raise RuntimeError(f"Process exited with code {exit_code}") + + # Load the results + if os.path.exists(results_path): + with open(results_path, "rb") as f: + results = pickle.load(f) + + # Check if an error was returned + if "error" in results: + raise RuntimeError(f"Program execution failed: {results['error']}") + + return results["centers"], results["radii"], results["sum_radii"] + else: + raise RuntimeError("Results file not found") + + except subprocess.TimeoutExpired: + # Kill the process if it times out + process.kill() + process.wait() + raise TimeoutError(f"Process timed out after {timeout_seconds} seconds") + + finally: + # Clean up temporary files + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + if os.path.exists(results_path): + os.unlink(results_path) + + +def evaluate(program_path): + """ + Evaluate the program by running it once and checking the sum of radii + + Args: + program_path: Path to the program file + + Returns: + Dictionary of metrics + """ + # Target value from the paper + TARGET_VALUE = 2.635 # AlphaEvolve result for n=26 + + try: + # For constructor-based approaches, a single evaluation is sufficient + # since the result is deterministic + start_time = time.time() + + # Use subprocess to run with timeout + centers, radii, reported_sum = run_with_timeout( + program_path, timeout_seconds=600 # Single timeout + ) + + end_time = time.time() + eval_time = end_time - start_time + + # Ensure centers and radii are numpy arrays + if not isinstance(centers, np.ndarray): + centers = np.array(centers) + if not isinstance(radii, np.ndarray): + radii = np.array(radii) + + # Validate solution + valid = validate_packing(centers, radii) + + # Check shape and size + shape_valid = centers.shape == (26, 2) and radii.shape == (26,) + if not shape_valid: + print( + f"Invalid shapes: centers={centers.shape}, radii={radii.shape}, expected (26, 2) and (26,)" + ) + valid = False + + # Calculate sum + sum_radii = np.sum(radii) if valid else 0.0 + + # Make sure reported_sum matches the calculated sum + if abs(sum_radii - reported_sum) > 1e-6: + print(f"Warning: Reported sum {reported_sum} doesn't match calculated sum {sum_radii}") + + # Target ratio (how close we are to the target) + target_ratio = sum_radii / TARGET_VALUE if valid else 0.0 + + # Validity score + validity = 1.0 if valid else 0.0 + + # Combined score - higher is better + combined_score = target_ratio * validity + + print( + f"Evaluation: valid={valid}, sum_radii={sum_radii:.6f}, target={TARGET_VALUE}, ratio={target_ratio:.6f}, time={eval_time:.2f}s" + ) + + return { + "sum_radii": float(sum_radii), + "target_ratio": float(target_ratio), + "validity": float(validity), + "eval_time": float(eval_time), + "combined_score": float(combined_score), + } + + except Exception as e: + print(f"Evaluation failed completely: {str(e)}") + traceback.print_exc() + return { + "sum_radii": 0.0, + "target_ratio": 0.0, + "validity": 0.0, + "eval_time": 0.0, + "combined_score": 0.0, + } + + +# Stage-based evaluation for cascade evaluation +def evaluate_stage1(program_path): + """ + First stage evaluation - quick validation check + """ + try: + # Use the simplified subprocess approach + try: + centers, radii, sum_radii = run_with_timeout(program_path, timeout_seconds=600) + + # Ensure centers and radii are numpy arrays + if not isinstance(centers, np.ndarray): + centers = np.array(centers) + if not isinstance(radii, np.ndarray): + radii = np.array(radii) + + # Validate solution (shapes and constraints) + shape_valid = centers.shape == (26, 2) and radii.shape == (26,) + if not shape_valid: + print(f"Invalid shapes: centers={centers.shape}, radii={radii.shape}") + return {"validity": 0.0, "error": "Invalid shapes"} + + valid = validate_packing(centers, radii) + + # Calculate sum + actual_sum = np.sum(radii) if valid else 0.0 + + # Target from paper + target = 2.635 + + # Simple combined score for stage 1 + combined_score = (actual_sum / target) if valid else 0.0 + + # Return evaluation metrics + return { + "validity": 1.0 if valid else 0.0, + "sum_radii": float(actual_sum), + "target_ratio": float(actual_sum / target if valid else 0.0), + "combined_score": float(combined_score), + } + + except TimeoutError as e: + print(f"Stage 1 evaluation timed out: {e}") + return {"validity": 0.0, "combined_score": 0.0, "error": "Timeout"} + except Exception as e: + print(f"Stage 1 evaluation failed: {e}") + print(traceback.format_exc()) + return {"validity": 0.0, "combined_score": 0.0, "error": str(e)} + + except Exception as e: + print(f"Stage 1 evaluation failed completely: {e}") + print(traceback.format_exc()) + return {"validity": 0.0, "combined_score": 0.0, "error": str(e)} + + +def evaluate_stage2(program_path): + """ + Second stage evaluation - full evaluation + """ + # Full evaluation as in the main evaluate function + return evaluate(program_path) diff --git a/examples/circle_packing/initial_program.py b/examples/circle_packing/initial_program.py new file mode 100644 index 000000000..cb4ea397e --- /dev/null +++ b/examples/circle_packing/initial_program.py @@ -0,0 +1,133 @@ +# EVOLVE-BLOCK-START +"""Constructor-based circle packing for n=26 circles""" +import numpy as np + + +def construct_packing(): + """ + Construct a specific arrangement of 26 circles in a unit square + that attempts to maximize the sum of their radii. + + Returns: + Tuple of (centers, radii, sum_of_radii) + centers: np.array of shape (26, 2) with (x, y) coordinates + radii: np.array of shape (26) with radius of each circle + sum_of_radii: Sum of all radii + """ + # Initialize arrays for 26 circles + n = 26 + centers = np.zeros((n, 2)) + + # Place circles in a structured pattern + # This is a simple pattern - evolution will improve this + + # First, place a large circle in the center + centers[0] = [0.5, 0.5] + + # Place 8 circles around it in a ring + for i in range(8): + angle = 2 * np.pi * i / 8 + centers[i + 1] = [0.5 + 0.3 * np.cos(angle), 0.5 + 0.3 * np.sin(angle)] + + # Place 16 more circles in an outer ring + for i in range(16): + angle = 2 * np.pi * i / 16 + centers[i + 9] = [0.5 + 0.7 * np.cos(angle), 0.5 + 0.7 * np.sin(angle)] + + # Additional positioning adjustment to make sure all circles + # are inside the square and don't overlap + # Clip to ensure everything is inside the unit square + centers = np.clip(centers, 0.01, 0.99) + + # Compute maximum valid radii for this configuration + radii = compute_max_radii(centers) + + # Calculate the sum of radii + sum_radii = np.sum(radii) + + return centers, radii, sum_radii + + +def compute_max_radii(centers): + """ + Compute the maximum possible radii for each circle position + such that they don't overlap and stay within the unit square. + + Args: + centers: np.array of shape (n, 2) with (x, y) coordinates + + Returns: + np.array of shape (n) with radius of each circle + """ + n = centers.shape[0] + radii = np.ones(n) + + # First, limit by distance to square borders + for i in range(n): + x, y = centers[i] + # Distance to borders + radii[i] = min(x, y, 1 - x, 1 - y) + + # Then, limit by distance to other circles + # Each pair of circles with centers at distance d can have + # sum of radii at most d to avoid overlap + for i in range(n): + for j in range(i + 1, n): + dist = np.sqrt(np.sum((centers[i] - centers[j]) ** 2)) + + # If current radii would cause overlap + if radii[i] + radii[j] > dist: + # Scale both radii proportionally + scale = dist / (radii[i] + radii[j]) + radii[i] *= scale + radii[j] *= scale + + return radii + + +# EVOLVE-BLOCK-END + + +# This part remains fixed (not evolved) +def run_packing(): + """Run the circle packing constructor for n=26""" + centers, radii, sum_radii = construct_packing() + return centers, radii, sum_radii + + +def visualize(centers, radii): + """ + Visualize the circle packing + + Args: + centers: np.array of shape (n, 2) with (x, y) coordinates + radii: np.array of shape (n) with radius of each circle + """ + import matplotlib.pyplot as plt + from matplotlib.patches import Circle + + fig, ax = plt.subplots(figsize=(8, 8)) + + # Draw unit square + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.set_aspect("equal") + ax.grid(True) + + # Draw circles + for i, (center, radius) in enumerate(zip(centers, radii)): + circle = Circle(center, radius, alpha=0.5) + ax.add_patch(circle) + ax.text(center[0], center[1], str(i), ha="center", va="center") + + plt.title(f"Circle Packing (n={len(centers)}, sum={sum(radii):.6f})") + plt.show() + + +if __name__ == "__main__": + centers, radii, sum_radii = run_packing() + print(f"Sum of radii: {sum_radii}") + # AlphaEvolve improved this to 2.635 + + # Uncomment to visualize: + visualize(centers, radii) diff --git a/examples/circle_packing/requirements.txt b/examples/circle_packing/requirements.txt new file mode 100644 index 000000000..067d4a6ea --- /dev/null +++ b/examples/circle_packing/requirements.txt @@ -0,0 +1,2 @@ +matplotlib +scipy \ No newline at end of file