A multi-objective evolutionary optimization library written in Rust. Implements NSGA-II (Non-dominated Sorting Genetic Algorithm II) for solving single- and multi-objective optimization problems over continuous, integer, and binary decision variables, with optional constraint handling, parallel (Rayon) evaluation, and optional GPU acceleration.
Python bindings: temporarily removed. They are being reimplemented in a later PR. This branch is Rust-only.
Full documentation: see GUIDE.md for walkthroughs, the complete API, and configuration reference.
Not published to crates.io yet — depend on it by path or git:
[dependencies]
rustypus = { path = "path/to/rustypus" }
# GPU evaluation is optional and off by default:
# rustypus = { path = "path/to/rustypus", features = ["gpu"] }use std::sync::Arc;
use rustypus::core::Problem;
use rustypus::gatypes::{Real, SolutionDataTypes};
use rustypus::genetic_algorithms_v2::{ExecutionMode, NSGAII};
// Objective: minimize two conflicting functions. Returns one value per objective.
fn objectives(x: &Vec<f64>) -> Vec<f64> {
let f1 = x[0] * x[0] + x[1] * x[1];
let f2 = (x[0] - 1.0).powi(2) + (x[1] - 1.0).powi(2);
vec![f1, f2]
}
fn main() {
let problem = Arc::new(Problem::new(
2, // solution_length (decision variables)
2, // number_of_objectives
None, // objective constraints
None, // constraint operators
Some(vec![-1, -1]), // direction: -1 = minimize, 1 = maximize
vec![
SolutionDataTypes::Real(Real::new(Some(-5.0), Some(5.0))),
SolutionDataTypes::Real(Real::new(Some(-5.0), Some(5.0))),
],
objectives,
));
let mut ga = NSGAII::new(Arc::clone(&problem), 100, ExecutionMode::MultiThreaded);
ga.run(10_000); // budget in objective-function evaluations (NFE); auto-initializes
for sol in ga.get_archive() {
println!("x = {:?} f = {:?}", sol.solution, sol.objective_fitness_values);
}
}get_archive() returns the non-dominated (Pareto-optimal) set. On a Solution, decision variables are sol.solution and objective values are sol.objective_fitness_values.
use rustypus::gatypes::{Real, Integer, BitBinary, SolutionDataTypes};
SolutionDataTypes::Real(Real::new(Some(-10.0), Some(10.0))) // continuous float in [-10, 10)
SolutionDataTypes::Integer(Integer::new(Some(-100), Some(100))) // integer in [-100, 100)
SolutionDataTypes::BitBinary(BitBinary::new()) // 0 or 1Mix them freely within one problem — crossover and mutation adapt per variable type. Defaults: Real → SBX crossover + uniform mutation; Integer → uniform crossover + uniform mutation; BitBinary → uniform crossover + bit-flip mutation.
Constraints are bounds on the objective values after evaluation: objective[i] <op> bound, where <op> is one of <, >, <=, >=, ==, !=. Pass one entry per objective (None = unconstrained).
let problem = Arc::new(Problem::new(
2,
1,
Some(vec![Some(1.0)]), // bound per objective
Some(vec![Some("<".to_string())]), // objective[0] must be < 1.0
Some(vec![-1]),
vec![
SolutionDataTypes::Real(Real::new(Some(-5.0), Some(5.0))),
SolutionDataTypes::Real(Real::new(Some(-5.0), Some(5.0))),
],
sphere,
));NSGA-II uses constraint-based dominance: a feasible solution dominates any infeasible one, and among infeasible solutions the one with fewer violations wins. sol.constraint_violation counts how many constraints a solution breaks; sol.feasible is true when none are broken.
Pass an ExecutionMode to NSGAII::new:
| Mode | What runs |
|---|---|
Sequential |
Single-threaded CPU |
MultiThreaded |
All CPU cores via Rayon (default) |
GPU |
GPU via a wgpu compute shader — see below |
Because GPU can silently fall back to CPU (feature off, or no evaluator attached) and a batch objective bypasses per-solution parallelism, ask the optimizer what it will actually do:
let ga = NSGAII::new(problem, 100, ExecutionMode::GPU);
assert_eq!(ga.effective_mode(), ExecutionMode::MultiThreaded); // no GPU evaluator attached → CPUBuild with --features gpu and attach a GpuEvaluator backed by a WGSL compute shader (see the shader interface documented at the top of src/gpu_evaluator.rs):
# // requires: features = ["gpu"]
use rustypus::gpu_evaluator::GpuEvaluator;
let evaluator = GpuEvaluator::new_blocking(shader_wgsl, solution_length, num_objectives);
let mut ga = NSGAII::new(problem, 200, ExecutionMode::GPU)
.with_gpu_evaluator(evaluator);
ga.run(50_000);On construction the evaluator prints the selected adapter to stderr (rustypus: GPU = ...), so you can confirm a real device is in use. GPU applies only to single-objective-function problems (EvalFn::Single); batch problems always run their batch closure on the CPU.
benchmark_objective_functions ships standard test functions usable directly as objectives: parabloid_5, parabloid_5_loc, parabloid_hyper_5, simple_objective, xyz_objective, and dtlz1–dtlz7 (note dtlz4 takes an extra alpha argument, so wrap it in a closure to use as an objective).
use rustypus::benchmark_objective_functions::dtlz2;
let problem = Arc::new(Problem::new(12, 3, None, None, Some(vec![-1; 3]),
types, dtlz2));The sandbox/ workspace has complete, runnable examples:
cd sandbox
cargo run --release --example generic_opt # ZDT1 bi-objective
cargo run --release --example portfolio_opt
cargo run --release --example supply_chain
cargo run --release --example bench_zdt1cargo test # core library (CPU paths)
cargo test --features gpu # also compiles/exercises the GPU-gated codeSee GUIDE.md for the full API reference, custom operators, dominance/sorting internals, and tuning tips.