Rust usage¶
colonyx exposes its optimization core as a Rust library and its Python
extension from the same codebase: src/algorithms/ and src/core/ contain
the actual algorithm implementations and shared problem/solution types, and
src/bindings.rs wraps them with PyO3 so the exact same
code compiles into both a standalone Rust crate and the colonyx._colonyx
extension module that colonyx/__init__.py re-exports from. maturin
builds that extension module.
Why the optimization core is written in Rust¶
Every algorithm here runs its inner loop — evaluate a population's fitness, update positions/velocities, sort or select, repeat — for potentially thousands of iterations across dozens of candidate solutions, which means tens of thousands to millions of small numeric operations per fit() call. That's exactly the workload where a native compiled loop with no per-iteration interpreter overhead, and no per-object allocation for things like particle positions, meaningfully outperforms the equivalent pure-Python loop; Rust also gets memory safety without a garbage collector, so continuous allocation/deallocation of populations across iterations doesn't add GC pause overhead into a hot loop the way it would in Python. This is why colonyx's algorithms live in Rust rather than Python from the ground up, instead of writing them in Python first and rewriting only proven bottlenecks later: for this workload, the hot path essentially is the whole algorithm. The tradeoff is that colonyx as a Rust crate has to expose Python-callable objectives through PyO3's callback mechanism (see the note on GIL and Python objectives below) whenever your objective function itself is written in Python rather than in Rust — that boundary crossing has real cost per call, which is worth knowing about if your objective function is cheap and called extremely often.
Add the crate¶
In a Rust project, add colonyx as a dependency:
Continuous optimization¶
Use Bounds, ContinuousProblem, and an optimizer such as ParticleSwarm:
use colonyx::algorithms::base::Optimizer;
use colonyx::algorithms::pso::ParticleSwarm;
use colonyx::core::{Bounds, ContinuousProblem};
fn main() {
let bounds = Bounds::uniform(3, -5.0, 5.0).unwrap();
let mut optimizer = ParticleSwarm::new(30, 100, 0.7, 1.5, 1.5, bounds);
optimizer.set_random_seed(Some(42));
let problem = ContinuousProblem {
name: "sphere".to_string(),
dimensions: 3,
objective_function: Box::new(|x: &[f64]| x.iter().map(|value| value * value).sum()),
};
optimizer.fit(&problem).unwrap();
let best = optimizer.predict().unwrap();
println!("best position: {:?}", best.variables);
println!("best score: {:?}", optimizer.score().unwrap());
}
Discrete optimization¶
Use AntColony with a distance matrix for TSP-style problems:
use colonyx::algorithms::aco::{AcoVariant, AntColony};
use colonyx::algorithms::base::Optimizer;
use colonyx::core::DiscreteProblem;
fn main() {
let mut optimizer = AntColony::new(
20,
100,
1.0,
3.0,
0.5,
100.0,
true,
AcoVariant::Basic,
0.9,
2.0,
0.1,
10.0,
);
let problem = DiscreteProblem {
name: "tsp".to_string(),
distance_matrix: vec![
vec![0.0, 1.0, 9.0, 9.0],
vec![1.0, 0.0, 1.0, 9.0],
vec![9.0, 1.0, 0.0, 1.0],
vec![9.0, 9.0, 1.0, 0.0],
],
};
optimizer.fit(&problem).unwrap();
println!("best tour: {:?}", optimizer.predict().unwrap().variables);
println!("tour length: {:?}", optimizer.score().unwrap());
}
Multi-objective optimization¶
Nsga2Optimizer and MopsoOptimizer implement MultiObjectiveOptimizer
(fit(&dyn MultiObjectiveProblem) / pareto_front() -> Vec<ParetoPoint>),
the vector-objective counterpart to Optimizer, so both can be held
polymorphically:
use colonyx::algorithms::{MopsoOptimizer, MultiObjectiveOptimizer, Nsga2Optimizer};
use colonyx::core::{Bounds, MultiObjectiveContinuousProblem};
let bounds = Bounds::uniform(1, -2.0, 3.0).unwrap();
let problem = MultiObjectiveContinuousProblem {
name: "biobjective".to_string(),
dimensions: 1,
objective_function: Box::new(|x: &[f64]| vec![x[0], (x[0] - 1.0).powi(2)]),
};
let mut optimizers: Vec<Box<dyn MultiObjectiveOptimizer>> = vec![
Box::new(Nsga2Optimizer::new(30, 30, 0.8, 0.2, 0.5, 30, bounds.clone())),
Box::new(MopsoOptimizer::new(20, 30, 0.7, 1.5, 1.5, 0.1, 20, bounds)),
];
for optimizer in optimizers.iter_mut() {
optimizer.fit(&problem).unwrap();
println!("{} points on the front", optimizer.pareto_front().len());
}
BinaryParticleSwarm implements the plain Optimizer trait the same way
(its fit_with_objective method is unchanged and still callable directly).
Parallel fitness evaluation¶
algorithms::base::evaluate_population(problem, population) evaluates a
batch of candidates via Rayon when there's more
than one. It's used for population initialization across the continuous
algorithms and for CMA-ES's per-generation evaluation, since CMA-ES samples
a whole generation before selecting on it — no candidate's fitness depends
on another's within the same batch, so parallelizing it changes nothing
about the result.
Only parallelize independent batches
evaluate_population is safe only where each candidate's fitness is
independent of every other candidate's in the same batch. Algorithms
with steady-state, per-individual acceptance (DE, ABC's employed/onlooker
phases) consume one candidate's result before generating the next, so
parallelizing those loops would change the algorithm's actual behavior,
not just its speed — don't do it there.
GIL and Python objectives
When the objective is a Python callable (via bindings::make_objective),
each evaluation re-acquires the GIL. The pyo3 binding releases the GIL
for the whole fit() call (py.allow_threads) so Rayon's worker threads
can each acquire it in turn — without that release, a worker blocking on
the GIL while the calling thread holds it deadlocks. Real speedup from
this still depends on the Python objective itself releasing the GIL
while it runs (e.g. NumPy/SciPy-backed code); a pure-Python objective
just serializes on the GIL again across threads. A native Rust Problem
has no such limit.
Notes¶
- The Python extension still lives at
colonyx._colonyx. colonyx::corecontains shared problem and solution types.colonyx::algorithmscontains the Rust implementations.
Building the extension yourself¶
If you're contributing to colonyx or just want to build from source rather than installing a wheel from PyPI, maturin develop builds the extension module and installs it into your active Python environment in one step — see Release for the exact commands, including the RUSTFLAGS needed on macOS for cargo build without maturin.
Where to next¶
- API Reference for the
Optimizer/MultiObjectiveOptimizer/Problemtrait signatures. - Release for building, packaging, and publishing both the PyPI wheel and the crates.io crate.
- Algorithms overview for what each algorithm actually does, independent of which language you call it from.