Skip to content

Latest commit

 

History

History
250 lines (178 loc) · 13.4 KB

File metadata and controls

250 lines (178 loc) · 13.4 KB
schedule every 6h
timeout-minutes 40

Circle Packing

Goal

Improve the circle packing constructor in .autoloop/programs/circle_packing/code/initial_program.py to maximize the sum of radii of 26 non-overlapping circles packed inside a unit square.

This is a classic computational geometry optimization problem. The AlphaEvolve paper achieved a sum of radii of 2.635 for n=26, which serves as the target. The initial implementation uses a naive concentric-ring layout (1 center + 8 inner ring + 16 outer ring) with greedy radius computation, achieving a much lower score.

Each iteration should improve the construct_packing() and compute_max_radii() functions inside the # EVOLVE-BLOCK-START / # EVOLVE-BLOCK-END markers. The algorithm must return exactly 26 circles, all within the unit square [0,1]x[0,1], with no overlaps.

Key geometric insights from the config's system message:

  • 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
  • Consider scipy.optimize.minimize with SLSQP for numerical refinement of positions

The evaluation validates the packing (no overlaps, all inside square, correct shapes) then computes:

  • sum_radii: The total sum of all 26 radii
  • target_ratio: sum_radii / 2.635 (how close to AlphaEvolve's result)
  • combined_score: target_ratio * validity (0 if any constraint is violated)

The metric is combined_score. Higher is better. A score of 1.0 means matching AlphaEvolve's result.

Target

Only modify these files:

  • .autoloop/programs/circle_packing/code/initial_program.py -- the packing constructor (only code between EVOLVE-BLOCK-START and EVOLVE-BLOCK-END)

Do NOT modify:

  • .autoloop/programs/circle_packing/code/evaluator.py -- the evaluation script
  • .autoloop/programs/circle_packing/code/config.yaml -- configuration for OpenEvolve
  • .autoloop/programs/circle_packing/code/requirements.txt -- dependencies
  • The run_packing, visualize functions outside the evolve block in initial_program.py

Evaluation

pip install -q numpy scipy matplotlib && python3 -c "
import importlib.util, numpy as np, json

spec = importlib.util.spec_from_file_location('program', '.autoloop/programs/circle_packing/code/initial_program.py')
program = importlib.util.module_from_spec(spec)
spec.loader.exec_module(program)

TARGET = 2.635
try:
    centers, radii, reported_sum = program.run_packing()
    centers, radii = np.array(centers), np.array(radii)

    # Validate shapes
    if centers.shape != (26, 2) or radii.shape != (26,):
        print(json.dumps({'combined_score': 0.0, 'error': 'invalid shapes'}))
        exit()

    # Validate no NaN
    if np.isnan(centers).any() or np.isnan(radii).any():
        print(json.dumps({'combined_score': 0.0, 'error': 'NaN values'}))
        exit()

    # Validate non-negative radii
    if (radii < 0).any():
        print(json.dumps({'combined_score': 0.0, 'error': 'negative radii'}))
        exit()

    # Validate inside unit square
    valid = True
    for i in range(26):
        x, y, r = centers[i][0], centers[i][1], radii[i]
        if x-r < -1e-6 or x+r > 1+1e-6 or y-r < -1e-6 or y+r > 1+1e-6:
            valid = False
            break

    # Validate no overlaps
    if valid:
        for i in range(26):
            for j in range(i+1, 26):
                dist = np.sqrt(np.sum((centers[i]-centers[j])**2))
                if dist < radii[i]+radii[j]-1e-6:
                    valid = False
                    break
            if not valid:
                break

    sum_radii = float(np.sum(radii)) if valid else 0.0
    ratio = sum_radii / TARGET if valid else 0.0
    score = ratio if valid else 0.0

    print(json.dumps({
        'combined_score': round(score, 6),
        'sum_radii': round(sum_radii, 6),
        'target_ratio': round(ratio, 6),
        'validity': 1.0 if valid else 0.0,
        'target': TARGET,
        'initial_score': 0.0
    }))
except Exception as e:
    print(json.dumps({'combined_score': 0.0, 'error': str(e)}))
"

The metric is combined_score from the JSON output. Higher is better. A score of 1.0 means matching AlphaEvolve's result of sum_radii = 2.635.

Evolution Strategy (inspired by OpenEvolve)

This section guides how the autoloop agent should approach proposing changes across iterations. The ideas are adapted from OpenEvolve's evolutionary programming framework.

Configuration

At the start of each iteration, read .autoloop/programs/circle_packing/code/config.yaml and use its values to drive the evolution strategy. Do not assume hard-coded values -- always read the config file fresh, as the user may tune parameters between runs.

The key config sections and how to use them:

  • database.population_size: Maximum number of candidate solutions to track. Scale down proportionally for autoloop.
  • database.archive_size: Number of elite solutions to always preserve. Scale similarly.
  • database.num_islands: Number of conceptual sub-populations. Each island represents a different packing strategy.
  • database.exploitation_ratio: Probability of choosing exploitation (refining top solutions) vs exploration.
  • database.elite_selection_ratio: Fraction of population considered "elite" for parent selection.
  • evaluator.cascade_evaluation and evaluator.cascade_thresholds: Multi-stage evaluation gates.
  • diff_based_evolution: If false (as in this config), full rewrites are preferred over diffs. This is because constructor functions benefit from holistic redesigns rather than incremental tweaks.
  • allow_full_rewrites: If true, the agent should feel free to completely rewrite the constructor each iteration.
  • prompt.system_message: Contains domain expertise and geometric insights. Read for inspiration.
  • prompt.num_top_programs: How many top solutions to review for inspiration.

1. Population Tracking

In repo memory, maintain a population of distinct packing solutions (sized proportionally to database.population_size from the config), each with:

  • code: The full evolve block content
  • combined_score: The evaluation metric
  • sum_radii: The actual sum of radii achieved
  • packing_strategy: A short label (e.g., "concentric_rings", "hexagonal_grid", "optimized_grid", "scipy_slsqp", "hybrid_construct_optimize")
  • code_complexity: Approximate line count of the evolve block
  • generation: Which iteration produced it
  • parent_id: Which population member it was derived from (null for the initial program)
  • island: Which conceptual island this solution belongs to

Store this in repo memory under autoloop/circle_packing as a population list alongside the existing history and rejected_approaches.

Maintain an elite archive sized proportionally to database.archive_size and database.elite_selection_ratio. Elite solutions are always preserved.

2. Island Model (Conceptual)

Read database.num_islands from the config and organize the population into that many conceptual islands, each representing a different packing strategy. For example, with 4 islands:

  • Island 0: Pure constructive approaches (grid layouts, hexagonal patterns, layered shells)
  • Island 1: Optimization-based approaches (scipy.optimize with SLSQP/COBYLA, gradient-based refinement)
  • Island 2: Hybrid approaches (construct initial layout, then optimize positions/radii numerically)
  • Island 3: Novel / experimental approaches (physics-based simulation, evolutionary within the constructor, greedy insertion)

When proposing a change:

  • Most iterations evolve within an island (refine the same strategy family)
  • Periodically (every ~5 iterations), attempt a migration: take the best technique from one island and incorporate it into a solution on another

3. Exploration vs Exploitation

Each iteration, read database.exploitation_ratio from the config and choose a strategy:

  • Exploitation (per config ratio): Take the current best-scoring solution and refine it. Adjust circle positions slightly, tune optimization parameters, improve the initial layout fed to an optimizer.
  • Exploration (most of the remainder): Try a fundamentally different packing strategy. If the population is all grid-based, try optimization-based. If all optimization, try a novel geometric construction.
  • Weighted random (small remainder): Pick any population member (weighted by score) and make a moderate change.

Record which strategy was used for each iteration in the history.

Important: Note that diff_based_evolution: false in this config. Unlike the other examples, this problem benefits from full rewrites of the constructor function. Small diffs to circle positions are unlikely to find good packings -- the geometry needs to be rethought holistically.

4. Cascade Evaluation

Read evaluator.cascade_evaluation and evaluator.cascade_thresholds from the config. If enabled, apply a mental pre-check:

  • Stage 1 (first threshold): Will this approach produce a valid packing at all? Will 26 circles fit without overlaps? If the geometric layout is clearly invalid, skip it.
  • Stage 2 (second threshold): After implementation, is the score competitive with the current best? If not, reject and record why.

5. Inspiration from Population

Read prompt.num_top_programs from the config. Draw inspiration from top and diverse population members:

  • Always review the best-scoring solutions
  • Review diverse solutions from different islands
  • For circle packing specifically, look at: What geometric pattern does each use? What optimization method? What's the radii distribution (many similar vs few large + many small)?
  • Explicitly note: "Inspired by population member X which uses [pattern/optimization]."

6. Constructor Design Principles

Circle packing has domain-specific considerations:

  • Validity is mandatory: Any overlap or boundary violation means score = 0. Always validate mentally before proposing.
  • Constructor vs optimizer: The evolve block should produce a deterministic result. You can use scipy.optimize inside the constructor, but the output must be reproducible.
  • Exact count: Must return exactly 26 circles. No more, no less.
  • Numerical precision: Use tolerances (1e-6) for overlap and boundary checks. Slightly shrink radii if needed to ensure validity.
  • Target awareness: AlphaEvolve achieved 2.635. Any solution above 2.5 is strong. Above 2.6 is excellent.
  • Radii distribution matters: The optimal packing likely has a mix of circle sizes, not all equal. Larger circles in the center, smaller ones filling gaps.

7. Novelty Check

Before implementing a proposed change, check against rejected_approaches and existing population:

  • If the approach is essentially the same as a rejected attempt, do not repeat it
  • If nearly identical to an existing population member, try something more distinct
  • Focus on meaningful differences: different geometric layouts, different optimization methods, different radii distributions

8. Feature Diversity (MAP-Elites Style)

Track solutions along two feature dimensions:

  • packing_strategy: The broad category (grid, hexagonal, optimized, hybrid, novel)
  • code_complexity: Simple (< 40 lines), medium (40-100 lines), complex (> 100 lines)

When the population is full, a new solution only enters if it fills an empty niche or beats the worst member in its feature cell.

9. Structured History in Reasoning

Before proposing each change, explicitly reason through:

  1. Config review: "Read config.yaml. exploitation_ratio=X, num_islands=Y, cascade_thresholds=Z, diff_based_evolution=W."
  2. Population state: "Current population has N members across M islands. Best sum_radii is X. Strategies represented: [list]. Under-explored niches: [list]."
  3. Strategy choice: "This iteration I will use [exploitation/exploration/weighted/migration]. Reason: [...]."
  4. Parent selection: "Starting from population member [id] (score: X, strategy: Y, island: Z)."
  5. Inspirations: "Drawing ideas from member [id] which uses [geometric pattern]."
  6. Validity pre-check: "This layout should be valid because [reasoning about no overlaps, boundary containment]."
  7. Novelty check: "This differs from rejected attempt [N] because [specific difference]."
  8. Proposed change: "I will [specific change] because [reasoning]."

10. Plateau Detection and Adaptation

If the last 3 consecutive iterations were rejected (no improvement):

  • Switch strategy: if you've been refining positions, try a completely different geometric layout
  • If pure construction is plateauing, add numerical optimization (scipy.optimize)
  • Consider combining the best geometric layout with the best optimization method from different population members

If the last 5 consecutive iterations were rejected:

  • The optimization may have plateaued
  • Try a radically different approach from an unexplored island
  • Re-read prompt.system_message from the config for geometric insights not yet exploited
  • Consider whether the current radii distribution is suboptimal (e.g., try fewer equal-sized circles with more small gap-fillers)