Tutorial 1: Your first optimization¶
This tutorial walks through the shortest real path from "nothing imported"
to "a finished, reproducible run": pick a benchmark problem, pick a
built-in algorithm class, call .run(), and read every field of the
result it returns. It exercises exactly what the
Quickstart shows in ten lines, slower and with every
field explained.
The problem: a BBOB benchmark handle¶
sezgi.bbob(fid, dim, instance) returns a native Problem handle for one
of the 24 noiseless BBOB/COCO functions — no subclassing needed, since
this problem's objective and known optimum already live compiled into the
Rust core.
import sezgi
problem = sezgi.bbob(1, 5, 1) # f1 = Sphere, dim=5, instance=1
print(type(problem))
fid=1 is the Sphere function (f(x) = sum(x_i^2), a smooth, unimodal,
easy baseline), dim=5 is the search dimension, instance=1 selects one
of BBOB's own randomized instance transformations (a fixed rotation/shift
applied to the base function — a different instance is a different,
still-Sphere-shaped, still-solvable problem; the same instance always
transforms the same way).
The algorithm: a wrapper class¶
Every built-in algorithm in sezgi is a class in sezgi.builtins, imported
directly off the top-level sezgi namespace. GeneticAlgorithm is one of
the ~30 preset-backed wrapper classes — its constructor takes algorithm
hyperparameters (pop_size here), and its .run() method takes the
per-run arguments (problem, budget, seed).
import sezgi
problem = sezgi.bbob(1, 5, 1)
algo = sezgi.GeneticAlgorithm(pop_size=20)
result = algo.run(problem, budget=1000, seed=1)
print(result)
SolveResult(algo='geneticalgorithm', seed=1, budget=1000, evals_used=1000, best_x=[2.5649048665803207, 1.8505248442574043, 3.322513036962495, 2.5426460403641102, -3.7109149583700916], best_f=-125.94250099117657, f_opt=-125.9497035670884, gap=0.007202575911833264)
GeneticAlgorithm auto-dispatches on the problem's own space kind: a BBOB
handle declares a continuous Float space, so this call delegates
internally to sezgi.presets.ga_real — a categorical or binary problem
would dispatch to a different preset of the same class instead (see
Built-in algorithm classes). budget=1000 is the
total number of objective-function evaluations this run is allowed;
seed=1 fixes every random draw the run makes, so the SAME
(problem, budget, seed) triple always reproduces the SAME result,
bit-for-bit, in this build and from the R frontend for a shared algorithm
(see Determinism and the RNG model).
Every field of the result¶
SolveResult — the return type of every wrapper class's .run() (and of
Algorithm.run()/AskTellAlgorithm.solve(), the two other front doors —
one shape everywhere except NSGA2, see Tutorial 7) — carries eight
fields:
import sezgi
problem = sezgi.bbob(1, 5, 1)
result = sezgi.GeneticAlgorithm(pop_size=20).run(problem, budget=1000, seed=1)
print(f"algo = {result.algo!r}")
print(f"seed = {result.seed}")
print(f"budget = {result.budget}")
print(f"evals_used = {result.evals_used}")
print(f"best_x[:3] = {result.best_x[:3]}")
print(f"best_f = {result.best_f:.6g}")
print(f"f_opt = {result.f_opt:.6g}")
print(f"gap = {result.gap:.6g}")
algo = 'geneticalgorithm' seed = 1 budget = 1000 evals_used = 1000 best_x[:3] = [2.5649048665803207, 1.8505248442574043, 3.322513036962495] best_f = -125.943 f_opt = -125.95 gap = 0.00720258
algo— the wrapper class's own name, lowercased ("geneticalgorithm"here), not the underlying preset it dispatched to. It identifies which wrapper class produced the result; forGeneticAlgorithm, the representation actually engaged ("real"for this Float-typed problem) is exposed separately as.dispatched_representationon theGeneticAlgorithminstance itself — not a field onSolveResult.seed,budget,evals_used— the run's own bookkeeping.evals_usedequalsbudgetunless the algorithm terminates early (no built-in class does today, but the field exists for algorithms that could).best_x— the best point this run actually evaluated, as a plainlist[float](dim=5, so 5 entries) for this Float-typed problem. Not guaranteed to lie within the problem's declared bounds for every algorithm (an unconstrained-step algorithm can propose outside them before the engine's own boundary repair runs — seesezgi.solve's own docstring on the Solve / compat internals page).best_f— the objective value atbest_x.f_opt— this problem's known optimum. BBOB functions ship an exact optimum, so this is a real float, notNone— a problem with no known optimum (Tutorial 4's local-search TSP instance, for example) reportsf_opt=Noneandgap=Noneinstead.gap—best_f - f_opt, the standard way to report a single run's quality without any cross-algorithm claim attached to it (see Comparing algorithms fairly for why a gap number alone is not a ranking).
A look at convergence¶
The figure below is generated by docs/scripts/fig_convergence_curve.py —
the SAME (problem, algorithm, seed) as above, re-run at 12 increasing
budgets, each budget its own independent .run() call.
GeneticAlgorithm's generator never reads the total budget while it runs
(unlike budget-adaptive presets such as lshade), so an independent run
at a smaller budget lands on the same point a longer run with the same
seed would have reached, whenever the two runs complete the same number
of generations — true at 11 of the 12 budgets plotted here (the one
exception, budget=450, stops one partial generation short of where a
longer run's own trajectory stood at evaluation #450, since the engine
only evaluates whole generation batches; see
Reading convergence curves
for a related but distinct technique — reconstructing an anytime view
from a SINGLE logged run instead of many independent ones):

The gap drops by roughly four orders of magnitude between budget=300 and
budget=2000 — the fast, smooth descent typical of a real-coded GA on an
easy, unimodal function like Sphere.
Next¶
- Defining your own Problem — write the objective yourself instead of using a shipped benchmark.
- Reading the result in the Quickstart covers the same eight fields more tersely.