Search
climber.search runs one search to the end, within a Budget, and leaves the result on the climber.
search is the whole search in one call, like fit on a model. It creates the run and the
search, lets the climber work until the budget runs out, and returns the climber with the result
on it. It is called search because that is what happens: the climber looks for a good
solution within the budget, and a better one may well exist.
from hillclimb import Budget, Climber, Problem
from hillclimb.policies import Greedy
from hillclimb.selectors import Best
problem = Problem("heilbronn-11")
budget = Budget(wall_clock="10m", evaluations=40) # the first limit reached ends the search
climber = Climber(selector_policy=Best(num_drafts=3), operator_policy=Greedy())
climber.search(
problem,
budget=budget,
agent="claude-code", # the coding agent that writes the solutions
model="sonnet",
learning=False, # keep this search out of the folder's knowledge
)
climber.best # and so on, see Results| argument | what it does |
|---|---|
problem | a Problem, or a problem id, a path, or a provider target like mlebench://… |
budget | a Budget, see below. A plain "10m" or a number of seconds means Budget(wall_clock=...). Without it, the folder's run defaults (budget.total_s in runs/config.yaml); with none there, the call raises |
agent, model | the coding agent and its model. Otherwise the folder's defaults |
holdout | score the hidden split if the problem has one. On by default |
learning | read from and write to the folder's knowledge. On by default |
seed_from | a solution.py to score first, as the floor the search has to beat |
name | the run's name |
config | a hillclimb.Config to use instead of the folder's hillclimb.yaml |
log | where the engine's log lines go. print by default, lambda *_: None for quiet |
hints | print where to follow the search live (hillclimb watch, tree, chart) when it starts. On by default; hints=False leaves the line out |
Ctrl-C stops it. Pressing Ctrl-C (or interrupting a notebook cell) ends the search as
stopped, prints how to resume it, and then raises KeyboardInterrupt, so a script that runs
several searches stops there instead of starting the next one. hillclimb resume continues it.
You need a hillclimb dir for this: a folder with a hillclimb.yaml somewhere above the working
directory. hillclimb init makes one. Anything you leave out comes from that folder's config.
The run lands in runs/ like any other, and its spec.yaml records what you passed, budget
included, so hillclimb run <run>/spec.yaml can run the same thing again.
The budget
A Budget says what a search may spend, in every dimension hillclimb counts. The first limit
reached ends the search.
from hillclimb import Budget
Budget(wall_clock="10m") # the clock alone
Budget(evaluations=30) # scored attempts, whatever the clock says
Budget(wall_clock="2h", evaluations=200, tokens=5_000_000, cost_usd=20)| dimension | what it counts |
|---|---|
wall_clock | wall-clock time, as "2h", "30m", "90s" or seconds. The search stops starting new work with a small margin left, so what is running can finish |
evaluations | scored attempts. Each attempt counts one, each tune trial counts one, the baseline is free |
tokens | the tokens the coding agent calls consumed, all kinds summed |
cost_usd | the coding agents' bill, where the agent reports one. Reaching it parks the search instead of ending it, so it can be resumed with more |
A dimension you leave out has no limit, except the wall clock, which falls back to the folder's run
defaults in runs/config.yaml (with no clock anywhere, a budget in another dimension gets 2 hours;
no budget at all is an error). The clock only counts while the search works. When you step a search by hand,
the time you spend reading an outcome is yours.
search returns the climber, so you can chain it:
best = Climber(operator_policy=Greedy()).search(problem, budget=budget).bestA climber can search again, the same problem or another one. Each call is a new search, and the climber's attributes always refer to the latest.
The same thing as a function
from hillclimb import run, run_spec
outcome = run("heilbronn-11", climber=climber, budget=Budget(wall_clock="10m")) # returns the SearchOutcome
outcomes = run_spec("run.yaml") # every entry of a run spec, in orderrun takes what search takes plus climber=, and returns the
SearchOutcome instead of the climber. run_spec runs a whole run spec
here, one entry after the other, where hillclimb run run.yaml would start a detached engine
per entry.