Ask/Tell Interface
The ask/tell interface inverts control of the optimization loop. Instead of
handing your objective function to search() and letting GFO drive
evaluation, you call ask() to get parameter sets, evaluate them yourself
in whatever way suits your environment, and call tell() to feed the
scores back. The optimizer’s algorithmic logic is identical to the
managed-loop interface; only the orchestration changes.
This is the right interface when you need to keep evaluation under your own control: parallel pools you already manage, asynchronous job queues, distributed clusters with their own scheduling, or integration into a larger optimization framework that wants to combine GFO algorithms with its own logic.
opt = Optimizer(search_space, initial_evaluations=[...])
params = opt.ask(n=4) # 1. Propose
scores = [evaluate(p) for p in params] # 2. Evaluate (your code)
opt.tell(scores) # 3. Report back
When to Use Which Interface
Both interfaces share the same 23 single-objective algorithms. The choice is about who runs the loop, not which optimizer you get.
Aspect |
|
ask/tell |
|---|---|---|
Loop control |
GFO drives evaluation |
You drive evaluation |
Initialization |
Built-in strategies (grid, random, vertices, warm_start) |
You supply |
Evaluation caching |
|
You handle caching yourself |
Progress and summary output |
|
None |
Stopping conditions |
|
You decide when to stop the loop |
Best result access |
|
|
Per-iteration results DataFrame |
|
Not available |
Constraints |
Supported |
Supported |
Random state |
Supported |
Supported |
Use search() when you have a self-contained Python function and want GFO
to handle everything around evaluation. Use ask/tell when evaluation lives
outside Python’s normal call flow, when you already have an evaluation
backend you want to keep, or when you are embedding GFO algorithms in a
larger system.
Basic Usage
The constructor requires initial_evaluations: a list of
(parameter_dict, score) pairs that seed the optimizer. After
construction the optimizer is in iteration state and ask / tell
can be called.
import numpy as np
from gradient_free_optimizers.ask_tell import HillClimbingOptimizer
search_space = {
"x": np.linspace(-10, 10, 100),
"y": np.linspace(-10, 10, 100),
}
def objective(params):
return -(params["x"] ** 2 + params["y"] ** 2)
initial_evaluations = [
({"x": 0.5, "y": 1.0}, objective({"x": 0.5, "y": 1.0})),
({"x": -3.0, "y": 0.0}, objective({"x": -3.0, "y": 0.0})),
]
opt = HillClimbingOptimizer(
search_space,
initial_evaluations=initial_evaluations,
)
for _ in range(50):
params = opt.ask(n=1)[0]
score = objective(params)
opt.tell([score])
print(opt.best_para)
print(opt.best_score)
Initial Evaluations
The initial_evaluations argument replaces the initialize strategies
(grid, random, vertices, warm_start) used by search(). You
provide the seed evaluations directly because the ask/tell interface has no
way to call your objective function on its own.
For local-search and SMBO algorithms, a single evaluation is enough to start.
Population-based optimizers (Particle Swarm, Genetic Algorithm, Evolution
Strategy, Differential Evolution, Parallel Tempering, Spiral Optimization,
CMA-ES) need at least one evaluation per population member, since each
sub-optimizer requires a starting point. The constructor enforces this and
raises ValueError if too few evaluations are supplied.
from gradient_free_optimizers.ask_tell import ParticleSwarmOptimizer
population = 10
initial_evaluations = [
(params, objective(params))
for params in random_starting_points(n=population)
]
opt = ParticleSwarmOptimizer(
search_space,
initial_evaluations=initial_evaluations,
population=population,
)
Batch ask/tell
ask(n=k) returns a list of k parameter dictionaries. tell expects
a list of k scores in the same order. The batch size you pass to ask
is independent of any internal algorithm parameter (population size,
neighbour count). For SMBO optimizers (Bayesian, TPE, Forest), batch
proposals are diversified internally so a single ask(n=8) does not return
eight near-identical points clustered around the acquisition peak.
Each ask must be followed by exactly one tell before the next
ask. Calling ask twice in a row raises RuntimeError; calling
tell with the wrong number of scores raises ValueError.
from gradient_free_optimizers.ask_tell import BayesianOptimizer
from concurrent.futures import ProcessPoolExecutor
opt = BayesianOptimizer(
search_space,
initial_evaluations=initial_evaluations,
)
with ProcessPoolExecutor(max_workers=4) as pool:
for _ in range(25):
params_batch = opt.ask(n=4)
scores = list(pool.map(objective, params_batch))
opt.tell(scores)
Constraints
Constraint functions work the same way as in search(). ask only
returns parameter sets that satisfy all constraints, so your evaluation
code does not need its own constraint check.
def inside_circle(params):
return params["x"] ** 2 + params["y"] ** 2 <= 25
opt = HillClimbingOptimizer(
search_space,
initial_evaluations=initial_evaluations,
constraints=[inside_circle],
)
Limitations
The ask/tell interface trades convenience for control. Several features that
search() provides have no counterpart here, by design:
The optimizer keeps no per-iteration history beyond what the algorithm needs
internally. There is no opt.search_data DataFrame, no convergence list,
no timing breakdown. If you want any of that, record it yourself in the loop.
There is no evaluation caching. If your objective is deterministic and you
expect duplicate proposals, deduplicate them before evaluation or wrap your
objective with functools.lru_cache.
There is no progress bar, no print_summary, no verbosity option. The optimizer does its work silently.
There are no built-in stopping conditions beyond your own loop. max_time,
max_score, early_stopping from search() are not exposed; you
implement them in your loop.
There are no callbacks and no catch parameter. Exception handling is your
responsibility.
Multi-objective optimizers (NSGA-II, MOEA/D, SMS-EMOA) are not exposed through ask/tell. Their score semantics differ from the single-objective case, and a clean ask/tell shape for them needs separate design.
Available Optimizers
All 23 single-objective algorithms from the main package are available with identical algorithmic behaviour:
Category |
Optimizers |
|---|---|
Local |
|
Global |
|
Population |
|
SMBO |
|
All ask/tell optimizers live in the gradient_free_optimizers.ask_tell
subpackage. The constructor signatures match the corresponding main-package
optimizers, with two differences: initialize is replaced by
initial_evaluations, and nth_process is not exposed.