Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
LLM4AD_API_KEY="Your LLM4AD Api key"
NEBIUS_API_KEY="Your Nebius API KEY"
313 changes: 313 additions & 0 deletions starter_ai_agents/llm_for_algorithm_design/llm4ad_v1/README.md

Large diffs are not rendered by default.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
from __future__ import annotations

from typing import Any
import numpy as np
from template import template_program, task_description
import itertools
from llm4ad.base import Evaluation

__all__ = ['CirclePackingEvaluation']


class CirclePackingEvaluation(Evaluation):
"""Evaluator for circle packing problem in a unit square."""

def __init__(self,
timeout_seconds=30,
**kwargs):
"""
Args:
timeout_seconds: Time limit for evaluation
n_instance: Number of problem instances to evaluate
max_circles: Maximum number of circles to pack (n)
Raises:
ValueError: If invalid parameters are provided
"""

super().__init__(
template_program=template_program,
task_description=task_description,
use_numba_accelerate=False,
timeout_seconds=timeout_seconds
)

self.n = 26

def evaluate_program(self, program_str: str, callable_func: callable) -> Any | None:
return self.evaluate(callable_func)

def verify_circles(self, circles: np.ndarray) -> bool:
"""Checks that the circles are disjoint and lie inside a unit square.

Args:
circles: A numpy array of shape (num_circles, 3), where each row is
of the form (x, y, radius), specifying a circle.

Returns:
bool: True if valid, False otherwise
"""
try:
# Check pairwise disjointness
for circle1, circle2 in itertools.combinations(circles, 2):
center_distance = np.sqrt((circle1[0] - circle2[0]) ** 2 + (circle1[1] - circle2[1]) ** 2)
radii_sum = circle1[2] + circle2[2]
if center_distance < radii_sum:
return False

# Check all circles lie inside the unit square [0,1]x[0,1]
for circle in circles:
if not (0 <= min(circle[0], circle[1]) - circle[2] and
max(circle[0], circle[1]) + circle[2] <= 1):
return False
return True
except Exception:
return False



def plot_circles(self,circles: np.ndarray):

import matplotlib.pyplot as plt
import matplotlib.patches as patches
"""Plots the circles."""
_, ax = plt.subplots(1, figsize=(7, 7))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect('equal') # Make axes scaled equally.

# Draw unit square boundary.
rect = patches.Rectangle((0, 0), 1, 1, linewidth=1, edgecolor='black', facecolor='none')
ax.add_patch(rect)

# Draw the circles.
for circle in circles:
circ = patches.Circle((circle[0], circle[1]), circle[2], edgecolor='blue', facecolor='skyblue', alpha=0.5)
ax.add_patch(circ)

plt.title(
f'A collection of {len(circles)} disjoint circles packed inside a unit square to maximize the sum of radii')
plt.show()

def evaluate(self, eva: callable) -> float:
"""Evaluate the circle packing solution."""
circles = eva(self.n)

#self.plot_circles(circles)
# Convert to numpy array if not already
circles = np.array(circles, dtype=np.float64)

# Verify the solution
if not self.verify_circles(circles) or len(circles) != self.n:
return -float('inf')

# Sum of radii is our score
score = np.sum(circles[:, 2])

return score
Comment on lines +93 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard evaluation against faulty candidate outputs

If a generated heuristic throws (e.g., returns None, ragged lists, or non-numeric values), the np.array(..., dtype=np.float64) cast will raise, bubbling up through the runner and killing the whole evaluation loop instead of yielding a safe failure score. We need to trap both the candidate invocation and the conversion to numpy, reject malformed shapes, and fall back to -inf. Otherwise a single bad sample can derail the entire workflow.

-        circles = eva(self.n)
-
-        #self.plot_circles(circles)
-        # Convert to numpy array if not already
-        circles = np.array(circles, dtype=np.float64)
-
-        # Verify the solution
-        if not self.verify_circles(circles) or len(circles) != self.n:
-            return -float('inf')
-
-        # Sum of radii is our score
-        score = np.sum(circles[:, 2])
+        try:
+            raw_circles = eva(self.n)
+        except Exception:
+            return -float('inf')
+
+        try:
+            circles = np.array(raw_circles, dtype=np.float64)
+        except (TypeError, ValueError):
+            return -float('inf')
+
+        if circles.ndim != 2 or circles.shape[0] != self.n or circles.shape[1] < 3:
+            return -float('inf')
+
+        circles = circles[:, :3]
+
+        # Verify the solution
+        if not self.verify_circles(circles):
+            return -float('inf')
+
+        # Sum of radii is our score
+        score = float(np.sum(circles[:, 2]))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
circles = eva(self.n)
#self.plot_circles(circles)
# Convert to numpy array if not already
circles = np.array(circles, dtype=np.float64)
# Verify the solution
if not self.verify_circles(circles) or len(circles) != self.n:
return -float('inf')
# Sum of radii is our score
score = np.sum(circles[:, 2])
return score
try:
raw_circles = eva(self.n)
except Exception:
return -float('inf')
try:
circles = np.array(raw_circles, dtype=np.float64)
except (TypeError, ValueError):
return -float('inf')
if circles.ndim != 2 or circles.shape[0] != self.n or circles.shape[1] < 3:
return -float('inf')
circles = circles[:, :3]
# Verify the solution
if not self.verify_circles(circles):
return -float('inf')
# Sum of radii is our score
score = float(np.sum(circles[:, 2]))
return score
🤖 Prompt for AI Agents
In
starter_ai_agents/llm_for_algorithm_design/llm4ad_v1.0/example_questions/circle_packing/EoH_settings&logs/evaluation.py
around lines 93-106, guard the candidate invocation and numpy conversion so
malformed outputs don't crash the runner: wrap circles = eva(self.n) in a
try/except and return -float('inf') on any exception or if it returns None; then
attempt conversion with np.asarray inside a try/except (or try np.array with
dtype=np.float64 and catch TypeError/ValueError), reject ragged or wrong-shaped
results by checking the array.ndim == 2 and shape[0] == self.n and shape[1] ==
3, ensure all values are finite and numeric (e.g., np.isfinite), and only then
call self.verify_circles and compute the score; on any failure or validation
error return -float('inf').







if __name__ == '__main__':

# import numpy as np
#
#
# def pack_circles(n: int) -> np.ndarray:
# """
# Pack n circles in a unit square to maximize sum of radii.
#
# Args:
# n: Number of circles to pack
#
# Returns:
# Numpy array of shape (n, 3) where each row is (x, y, radius)
# All values should be between 0 and 1
# Circles must not overlap
# """
#
# grid_size = int(np.ceil(np.sqrt(n)))
# radius = 0.5 / grid_size
#
# circles = []
# for i in range(n):
# row = i // grid_size
# col = i % grid_size
# x = (col + 0.5) / grid_size
# y = (row + 0.5) / grid_size
# circles.append([x, y, radius])
#
# return np.array(circles)
import numpy as np
import math


def pack_circles(n: int) -> np.ndarray:
"""
Pack n circles in a unit square to maximize sum of radii.

Args:
n: Number of circles to pack

Returns:
Numpy array of shape (n, 3) where each row is (x, y, radius)
All values should be between 0 and 1
Circles must not overlap
"""
if n == 0:
return np.zeros((0, 3))

circles = np.zeros((n, 3))
circles[0] = [0.5, 0.5, 0.5] # Place first circle at center with max possible radius

for i in range(1, n):
max_r = 0
best_pos = (0, 0)

# Grid search for best position
grid_size = 100
for x in np.linspace(0, 1, grid_size):
for y in np.linspace(0, 1, grid_size):
# Calculate minimum distance to existing circles and boundaries
min_dist = min(
min(np.sqrt((x - cx) ** 2 + (y - cy) ** 2) - cr for cx, cy, cr in circles[:i]),
x,
1 - x,
y,
1 - y
)

if min_dist > max_r:
max_r = min_dist
best_pos = (x, y)

circles[i] = [best_pos[0], best_pos[1], max_r]

return circles


pack = CirclePackingEvaluation()
pack.evaluate_program('_', pack_circles)

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations
import os
import sys
from pathlib import Path

# Derive project root and ensure it's on sys.path before any llm4ad imports
BASE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = BASE_DIR.parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.append(str(PROJECT_ROOT))
Comment on lines +6 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure the in-repo llm4ad takes precedence on sys.path.

Using sys.path.append lets any site-packages installation of llm4ad shadow the freshly added v1.0 code, which can break the script or load mismatched APIs. Insert the project root at the front so the local checkout always wins.

 if str(PROJECT_ROOT) not in sys.path:
-    sys.path.append(str(PROJECT_ROOT))
+    sys.path.insert(0, str(PROJECT_ROOT))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Derive project root and ensure it's on sys.path before any llm4ad imports
BASE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = BASE_DIR.parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.append(str(PROJECT_ROOT))
# Derive project root and ensure it's on sys.path before any llm4ad imports
BASE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = BASE_DIR.parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
🤖 Prompt for AI Agents
In
starter_ai_agents/llm_for_algorithm_design/llm4ad_v1.0/example_questions/circle_packing/EoH_settings&logs/run_eoh.py
around lines 6 to 10, sys.path.append is used which lets installed packages
shadow the local checkout; change this to ensure the project root is placed at
the front of sys.path (use sys.path.insert(0, str(PROJECT_ROOT))). If the path
may already exist, remove any existing occurrence and then insert it at index 0
so the in-repo llm4ad always takes precedence.


from evaluation import CirclePackingEvaluation
# from llm4ad.tools.llm.llm_api_https import HttpsApi
from starter_ai_agents.llm_for_algorithm_design.llm4ad_v1.llm_nebuis import NebuisLLM
from llm4ad.method.eoh import EoH,EoHProfiler

from dotenv import load_dotenv

def main():
"""
Run EoH on CirclePackingEvaluation
"""
llm = NebuisLLM(host='api.bltcy.ai', # your host endpoint, e.g., 'api.openai.com', 'api.deepseek.com'
key=os.getenv("LLM4AD_API_KEY"), # your key
model='deepseek-v3', # your llm, e.g., 'gpt-3.5-turbo'
timeout=120)

task = CirclePackingEvaluation(timeout_seconds=1200) # local

method = EoH(llm=llm,
profiler=EoHProfiler(log_dir='logs/eohseed', log_style='simple'),
evaluation=task,
max_sample_nums=15000,
max_generations=10000,
pop_size=32,
num_samplers=32,
num_evaluators=32,
debug_mode=False)

method.run()


if __name__ == '__main__':
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
template_program = '''
import numpy as np
import math
def pack_circles(n: int) -> np.ndarray:
"""
Pack n circles in a unit square to maximize sum of radii.

Args:
n: Number of circles to pack

Returns:
Numpy array of shape (n, 3) where each row is (x, y, radius)
All values should be between 0 and 1
Circles must not overlap

Important: Set "all" random seeds to 2025, including the packages (such as scipy sub-packages) involving random seeds.
"""

grid_size = int(np.ceil(np.sqrt(n)))
radius = 0.5 / grid_size

circles = []
for i in range(n):
row = i // grid_size
col = i % grid_size
x = (col + 0.5) / grid_size
y = (row + 0.5) / grid_size
circles.append([x, y, radius])

return np.array(circles)
'''

task_description = "Implement a function that uses a constructive heuristic to pack n non-overlapping circles iteratively within a unit square to maximize the sum of their radii"
Loading