Class hierarchy¶
sezgi's Python surface has three independent authoring paths into the same
Rust engine, plus one large family of ready-to-use wrapper classes. They
are not one inheritance tree — AskTellAlgorithm in particular is a
parallel base, not a subclass or superclass of Algorithm, because it
drives a completely different execution model (see
The Python-callback bridge
below for why).
The four families¶
sezgi.Problem(problem.py) — an ABC you subclass to declare a search space (space()) and an objective (evaluate(x)); optionally overrideoptimum()/batch_evaluate().sezgi.recipes.FeatureSelectionandsezgi.recipes.MixedTuning(recipes.py) are its two concrete subclasses shipped in this milestone.sezgi.Algorithm(algorithm.py) — an ABC whosegenerate(pop, ctx)hook (required) runs inside the Rust engine loop, once per stage per generation, via_sezgi.solve_with_py_generator'sPyGeneratorbridge. Two family bases sit below it:PopulationAlgorithm(addsselect/vary, composing them intogenerate) andLocalSearch(addsneighbor/accept, pinned topop_size=1).sezgi.AskTellAlgorithm(algo.py, formerly the top-levelsezgi.Algorithmbefore this milestone's name repurposing) — an ABC whosesetup(ctx)/step(ctx)hooks drive a pure-Python loop over the ask/tellAlgoContext/EvalSessioncore, entirely outside the Rust engine's own generation loop. Population bookkeeping, replacement, and termination are all the subclass's own responsibility here — the engine only supplies evaluation counting, budget enforcement, and a deterministic RNG stream.- The 29 built-in wrapper classes (
builtins.py) — one percrates/components/src/presets.rspreset builder (34 builders; some presets, like DE's three variants, are collapsed under one wrapper via avariant=kwarg), plusNSGA2(a thinsezgi.mo.nsga2skin, not apresets.rsbuilder). None of them is aProblem/Algorithmsubclass — all 29 are plainobjectsubclasses. 28 of the 29 share one uniform shape:__init__(pop_size=..., **preset_kwargs)thenrun(problem, budget, seed=0, ...), internally building anAlgorithmSpecand callingsezgi.solve()(the same compat-layer entry pointsezgi.Algorithm.runfunnels its own result through, via the shared_wrap_resulthelper inalgo.py).NSGA2is the one exception: its__init__/run()signatures mirrorsezgi.mo.nsga2's own multi-objective parameter set instead (run(problem, dim, budget, m=None, seed=0, k=None, l=None, ...)), and — per its own class docstring —run()returnsmo.nsga2's own raw dict (individuals,objectives,front0,evals_used,violationswhen constrained), notsezgi.algo.SolveResult: that dataclass's fields (best_x/best_f/f_opt/gap) assume a single-objective run with one best point, which does not fit a multi-objective Pareto front.
sezgi.Algorithm, sezgi.AskTellAlgorithm, and 28 of the 29 built-in
wrapper classes converge on one result shape: sezgi.algo.SolveResult
(algo name, seed, budget, evals_used, best_x, best_f, f_opt,
gap). NSGA2 is the documented exception to that convergence — see
above.
As a class diagram¶
classDiagram
class Problem {
<<ABC>>
+evaluate(x) float
+space() Space
+optimum() float|None
+batch_evaluate(xs) list~float~
}
Problem <|-- FeatureSelection
Problem <|-- MixedTuning
class Algorithm {
<<ABC>>
+generate(pop, ctx) iterable
+initialize(n, ctx) iterable
+validate_space(space)
+run(problem, budget, seed, pop_size, ...) SolveResult
}
Algorithm <|-- PopulationAlgorithm
Algorithm <|-- LocalSearch
class PopulationAlgorithm {
+select(pop, k, ctx) list
+vary(parents, ctx) iterable
}
class LocalSearch {
+neighbor(x, ctx) x
+accept(f_old, f_new, ctx) bool
}
class AskTellAlgorithm {
<<ABC, PARALLEL base -- NOT related to Algorithm>>
+setup(ctx)
+step(ctx)
+solve(problem, budget, seed) SolveResult
}
class BuiltinWrapper {
<<28 of 29 classes, e.g. GeneticAlgorithm,\nDifferentialEvolution, ParticleSwarm,\nRandomSearch, SimulatedAnnealing, ...>>
+__init__(pop_size, **preset_kwargs)
+run(problem, budget, seed, run_id, log_dir) SolveResult
}
class NSGA2 {
<<the 1 exception -- thin skin over sezgi.mo.nsga2>>
+__init__(pop_size, eta_c, eta_m, ...)
+run(problem, dim, budget, m, k, l, seed, ...) dict
}
class SolveResult {
<<shared result shape>>
algo
seed
budget
evals_used
best_x
best_f
f_opt
gap
}
class MoNsga2Dict {
<<mo.nsga2's own dict, NOT SolveResult>>
individuals
objectives
front0
evals_used
violations
}
Algorithm ..> SolveResult : run() returns
AskTellAlgorithm ..> SolveResult : solve() returns
BuiltinWrapper ..> SolveResult : run() returns
NSGA2 ..> MoNsga2Dict : run() returns
The compat layer underneath¶
Three of these four families are genuinely one implementation underneath:
sezgi.Algorithm.run, the 29 built-in wrapper classes' run(), and a
hand-written spec passed straight to sezgi.solve() are different front
doors onto the same sezgi.solve() / _sezgi.solve_with_py_generator()
compat internals, which build an AlgorithmSpec and hand it to the one
Engine::run loop described in Engine flow.
sezgi.AskTellAlgorithm is not part of that convergence — as its own
description above says, AskTellAlgorithm.solve() drives a pure-Python
setup()/step() loop directly over EvalSession's ask/tell core; it
never builds an AlgorithmSpec and never calls Engine::run or
solve_with_py_generator at all. The engine only supplies that session's
own evaluation counting, budget enforcement, and deterministic RNG stream
— the generation loop itself is entirely AskTellAlgorithm's own Python
code. See Solve / compat internals for the three
converging families' own reference.
Next¶
- Determinism and the RNG model explains how
Algorithm.run'sgenerate()callback draws from the exact same seeded stream a built-in RustGeneratorwould have used at that call.