Getting started¶
This tutorial walks from a one-liner to comparing algorithms. It assumes you
have run maturin develop (see Installation).
1. Your first optimization¶
import turboswarm as pso
# Minimize the Rastrigin benchmark in 2D. The function runs natively in Rust.
r = pso.minimize("rastrigin", bounds=[(-5.12, 5.12)] * 2, seed=42)
print(r.best_value) # ~0.0
print(r.best_position) # ~[0.0, 0.0]
bounds defines the search box. Two equivalent ways:
# One (min, max) per dimension — each dimension can have its own range:
pso.minimize(f, bounds=[(-5, 5), (0, 100)]) # 2-D, different ranges
# A single (min, max) for every dimension — pass dim:
pso.minimize(f, bounds=(-5.12, 5.12), dim=10) # 10-D, same range
The seed makes the run reproducible.
2. Your own objective¶
Pass any Python callable f(list[float]) -> float:
# Shifted sphere: optimum at (2, 2, 2), value 0.
r = pso.minimize(lambda x: sum((xi - 2) ** 2 for xi in x),
bounds=[(-10, 10)] * 3, seed=1)
A native benchmark runs without the GIL and is faster; a Python callable is re-entered through the GIL on every evaluation but is fully flexible.
Vectorized objectives¶
With vectorized=True, the objective receives the whole swarm per call (a
list of rows, n_particles x dim) and returns one value per row. This reduces
the Python round-trips from one-per-particle to one-per-iteration:
import numpy as np
r = pso.minimize(lambda X: np.sum(np.asarray(X) ** 2, axis=1),
bounds=[(-5, 5)] * 10, vectorized=True, seed=0)
The swarm is handed to your objective as a NumPy array (n_particles x dim),
built from a contiguous buffer — no per-element Python objects.
Honest expectations
For an expensive, vectorizable objective this matches a fully
NumPy-based library (measured on par with pyswarms). For cheap
objectives the per-iteration framework overhead dominates and an all-NumPy
library can still be ~1.5–2× faster, since it also vectorizes the swarm
bookkeeping. See Comparison.
3. Choosing a variant and a topology¶
r = pso.minimize("ackley", bounds=(-32.768, 32.768), dim=2,
velocity="fips", topology="ring", seed=1)
velocity:"inertia"(default),"constriction","fips"— see Variants.topology:"global"(default),"ring","vonneumann"— see Topologies.
4. Reading the result¶
minimize returns a PsoResult:
| Attribute | Meaning |
|---|---|
best_position |
best point found (integers if integer=True) |
best_value |
objective value at best_position |
convergence |
best value after each iteration (the convergence curve) |
history |
history[iter][particle][dim], for animation (empty if record_history=False) |
5. Comparing algorithms¶
runs = {
"inertia/global": pso.minimize("rastrigin", bounds=[(-5.12, 5.12)] * 2,
velocity="inertia", topology="global", seed=7),
"fips/ring": pso.minimize("rastrigin", bounds=[(-5.12, 5.12)] * 2,
velocity="fips", topology="ring", seed=7),
}
for name, r in runs.items():
print(name, r.best_value)
# inertia/global ≈ 2e-10, fips/ring ≈ 2e-4 — both essentially reach the optimum (f = 0)
To plot the comparison, see Visualization. For a
full feature tour, run python examples/tour.py (visualization is optional,
behind --plot / --animate).
6. Run control: early stopping and velocity clamp¶
Stop early once the swarm stops improving, instead of always running every iteration:
# Stop when the best value does not improve by more than tol for 20
# consecutive iterations.
r = pso.minimize("sphere", bounds=[(-5.12, 5.12)] * 2,
max_iter=1000, patience=20, tol=1e-12, seed=42)
print(len(r.convergence)) # usually far fewer than 1000
Clamp the per-component velocity (a classic way to curb overshooting):
Other stop conditions: a target value, an evaluation budget, or a wall-clock budget:
r = pso.minimize("sphere", bounds=[(-5.12, 5.12)] * 2, max_iter=10000,
target=1e-6, # stop once best_value <= 1e-6
max_evals=50_000, # ...or after 50k objective evaluations
max_time=2.0, # ...or after 2 seconds
seed=42)
print(r.stop_reason) # "target" | "max_evaluations" | "max_time" | ...
print(r.evaluations) # objective evaluations performed
The result reports stop_reason and evaluations. All of these default to
off, so the standard behavior is unchanged unless you opt in.
A callback runs once per iteration — handy for live logging or custom
stopping. It receives (iteration, best_value); return False to stop early: