Walkthrough
The Heilbronn triangle problem end to end, with the figures the run produced.
Five steps, one problem: 13 points in the plane, scored by the smallest triangle they form
relative to their convex hull. Every figure below is from the run itself — the same data
hillclimb chart, hillclimb archive and hillclimb graph draw in the terminal.
01 — Specify the problem
You specify the problem by writing the verify.py file. For the Heilbronn problem, the scorer
looks something like the following:
# 13 points in the plane; score = min triangle area / convex hull area
N, N_TRIANGLES = 13, 286
def fail(reason):
write_report(0.0, report_error=reason) # invalid configurations score 0
sys.exit(0)
df = pd.read_csv("submission.csv") # id,x,y
if len(df) != N or sorted(df["id"].tolist()) != list(range(N)):
fail(f"need exactly {N} rows with id 0..{N - 1}")
points = df.sort_values("id")[["x", "y"]].to_numpy(float)
if not np.all(np.isfinite(points)):
fail("coordinates must be finite")
# translation and uniform scaling leave the score unchanged; normalize
# so very large or very small valid coordinates stay numerically safe
points = points - points[0]
points = points / np.max(np.abs(points))
hull_area = polygon_area(convex_hull(points))
if hull_area <= np.finfo(float).eps:
fail("degenerate convex hull")
# the smallest of all C(13,3) = 286 triangles, relative to the hull
min_area = min(
0.5 * abs(cross(points[i], points[j], points[k]))
for i, j, k in combinations(range(N), 3)
)
write_report(min_area / hull_area) # -> $HILLCLIMB_RESULTExiting 0 means the candidate is valid, and whatever the scorer writes to $HILLCLIMB_RESULT is
its score: a bare number, or {"score": 0.0309}. The
full file
also reports the six smallest triangles and their areas, so an improve operator sees where the
configuration is weakest, not just the final number. Tip: make the verifier hard to fool: whatever
it fails to check, the search will eventually exploit.
A config file in the same folder problem.yaml names the metric, the winning direction, and any
baseline scores the chart draws as reference lines:
problem_id: heilbronn-convex-13
metric: normalized-min-triangle-area
higher_is_better: true
description: description.md
baseline: baseline.py
baseline_files: {submission.csv: sample_submission.csv}
chart_baselines:
"OpenEvolve (GPT-5, 100 candidates)": 0.0267
"AdaEvolve (GPT-5, 100 candidates)": 0.0290
"AlphaEvolve": 0.030936889034895654
time_budget_s: 1800
allow_network: false02 — Give it a budget, the model and start a search
The budget is wall-clock time: hillclimb keeps drafting and scoring candidates until it is
spent, and that is the only stopping rule. --agent picks the coding agent that writes the
candidates (claude-code or codex) and --model the model it uses; both can be set once in
hillclimb/config.yaml and left off the command. Everything a search produces, from every
candidate's code to its logs and scores, lands under hillclimb/runs/.
hillclimb run heilbronn-convex-13 \
--budget 30m \
--parallel-searches 2 \
--parallel-agents 3 \
--agent claude-code \
--model claude-sonnet-5Agents now take turns as operators: draft a new approach, debug one that crashed,
improve the best scorer so far, ensemble the survivors at the end. Every new candidate runs
through your verifier, and the best solution.py is kept in the search's best/ folder.
03 — Watch it climb
Now watch the agent work with:
hillclimb watch # the tree, live
hillclimb chart # the curve belowevery search of the problem, as hillclimb chart plots it: each dot is a scored candidate in landing order, the line is the best score so far across all searches, and the published scores from problem.yaml are the reference lines
the same search as an archive: one circle per candidate, filled by score and ringed by what the search did with it, beside the progress chart. drag the slider or press play to watch both grow
04 — Inspect the solution
What a search leaves behind is code. best/solution.py is the winning program and
best/submission.csv is what it produced. This search's winner is the draft itself: a
two-phase global search that polishes structured and random starts across hull sizes 8–13 with an
SLSQP solve (maximize the smallest triangle area with the hull pinned to area 1), then perturbs the
leader until time runs out. Both parallel searches converged on the same configuration.
"""Multi-start global search over diverse hull/interior topologies for
the normalized Heilbronn problem (13 points), each candidate polished
with an epigraph SLSQP formulation (maximize t = min triangle area
subject to a fixed convex-hull area of 1), keeping the best
true-scored configuration found.
"""
def true_score(pts):
area, hull_idx = hull_area_and_indices(pts) # scipy ConvexHull
tri = triangle_areas(pts) # all 286 of them
return float(tri.min() / area)
def polish(pts0, maxiter=150):
# pin every triangle's orientation, then push up the smallest
# signed area as an epigraph variable t
pts_g, hull_idx = gauge_normalize(pts0) # hull area -> 1
signs = np.sign(triangle_crosses(pts_g))
def tri_cons(x):
pts, t = unpack(x)
return signs * triangle_crosses(pts) * 0.5 - t # area_i >= t
res = minimize(neg_t, x_init, jac=neg_t_grad, method="SLSQP",
constraints=[{"type": "eq", "fun": hull_eq},
{"type": "ineq", "fun": tri_cons}],
options={"maxiter": maxiter, "ftol": 1e-14})
return unpack(res.x)[0]
def main():
best_pts = polish_iterated(REF_PTS, rounds=3) # known-good seed
best_score = true_score(best_pts)
# phase 1: broad exploration across hull/interior topologies
for kind, hull_size in starters: # hull sizes 8..13
if time.time() > phase1_deadline:
break
pts = polish_iterated(make_start(kind, hull_size), rounds=2)
if pts is not None and true_score(pts) > best_score:
best_pts, best_score = pts, true_score(pts)
# phase 2: basin-hopping local search around the current best
while time.time() - start < TIME_BUDGET_S:
pts = polish_iterated(perturb(best_pts), rounds=2)
if pts is not None and true_score(pts) > best_score:
best_pts, best_score = pts, true_score(pts)
# final tight refinement of the winner, then write it out
best_pts = polish_iterated(best_pts, rounds=4, maxiter=400)
write_csv("submission.csv", best_pts)its submission.csv, drawn: 13 points, ten on the hull and three interior. The shaded triangle is the smallest of the 286; its area over the hull's is 0.0309372, where AlphaEvolve reported 0.0309369
05 — See what it learns
Every search writes what it learned into a graph: claims, techniques, libraries, and the problems
it ran on. Later searches read it back, so the next one does not start from nothing. Below is an
example of a hillclimbrun solving the Heilbronn problem. Slide back through time and each search
is a tick where new claims appear.
drag rotate · shift-drag pan · scroll zoom · click a type to hide · slide to scrub time