swaits 9d46cf9d65 feat(algorithms): add SPEA2 (Strength Pareto Evolutionary Algorithm 2)
Implementation of Zitzler, Laumanns, Thiele 2001 SPEA2 — the classic
Pareto MOEA built around an explicit external archive of fixed size.

Each generation:
1. Combine the current population and the archive into one pool.
2. For every member, compute strength S(i) = number of others that
   member dominates, then raw fitness R(i) = sum of S(j) over members
   j that dominate i.
3. Add a density estimator D(i) = 1/(σ_k + 2) where σ_k is the distance
   to the k-th nearest neighbor (k = floor(sqrt(|pool|))) in
   minimization-oriented objective space.
4. Final fitness F(i) = R(i) + D(i); lower is better.
5. Build the next archive by taking every non-dominated member
   (R(i) == 0). If too many, prune by repeatedly removing the member
   with the smallest k-th-nearest-neighbor distance. If too few, fill
   from the rest sorted by F ascending.
6. Generate the next population by binary tournament on F (lower wins),
   then variation, then evaluation.

Public API mirrors the other algorithms:

  Spea2Config { population_size, archive_size, generations, seed }
  Spea2 { config, initializer, variation }
  impl<P, I, V> Optimizer<P> for Spea2<I, V>

Re-exported from the prelude. Tests cover archive size invariants,
non-empty Pareto front on Schaffer N.1, deterministic reruns under
the same seed, and panic on population_size == 0.
2026-05-04 19:52:47 -06:00

heuropt

A practical Rust toolkit for implementing heuristic single-objective, multi-objective, and many-objective optimization algorithms.

heuropt is not a research framework full of abstract machinery — it is a small set of concrete types, a handful of simple traits, and a few reference algorithms. The goal: an entry-level Rust engineer can define a problem, run a built-in optimizer, or implement a new optimizer without learning any framework concepts.

Installation

[dependencies]
heuropt = "0.1"

# Optional features:
# - "serde":     derive Serialize/Deserialize on the core data types.
# - "parallel":  evaluate populations across rayon's thread pool.
#                Seeded runs stay bit-identical to serial mode.
# heuropt = { version = "0.1", features = ["serde", "parallel"] }

Define a problem

use heuropt::prelude::*;

struct SchafferN1;

impl Problem for SchafferN1 {
    type Decision = Vec<f64>;

    fn objectives(&self) -> ObjectiveSpace {
        ObjectiveSpace::new(vec![
            Objective::minimize("f1"),
            Objective::minimize("f2"),
        ])
    }

    fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
        let v = x[0];
        Evaluation::new(vec![v * v, (v - 2.0).powi(2)])
    }
}

Run NSGA-II

use heuropt::prelude::*;

# struct SchafferN1;
# impl Problem for SchafferN1 {
#     type Decision = Vec<f64>;
#     fn objectives(&self) -> ObjectiveSpace {
#         ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
#     }
#     fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
#         Evaluation::new(vec![x[0] * x[0], (x[0] - 2.0).powi(2)])
#     }
# }
let initializer = RealBounds::new(vec![(-5.0, 5.0)]);
let variation = GaussianMutation { sigma: 0.2 };
let config = Nsga2Config { population_size: 60, generations: 80, seed: 42 };
let mut optimizer = Nsga2::new(config, initializer, variation);
let result = optimizer.run(&SchafferN1);

println!("Pareto front size: {}", result.pareto_front.len());

See examples/toy_nsga2.rs for the full version.

Implement a custom optimizer

A new optimizer is just an implementation of Optimizer<P>:

use heuropt::prelude::*;

struct MyOptimizer { /* state */ }

impl<P> Optimizer<P> for MyOptimizer
where
    P: Problem<Decision = Vec<f64>>,
{
    fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
        // Generate candidates.
        // Evaluate them with `problem.evaluate(...)`.
        // Keep the best, or maintain a Pareto archive.
        // Return an OptimizationResult.
        # OptimizationResult::new(
        #     Population::new(Vec::new()),
        #     Vec::new(),
        #     None,
        #     0,
        #     0,
        # )
    }
}

A complete worked example is in examples/custom_optimizer.rs.

Current algorithms

  • RandomSearch — sample-evaluate-keep baseline.
  • Paes — a small (1+1) Pareto Archived Evolution Strategy.
  • Nsga2 — the canonical Pareto-based evolutionary algorithm.
  • DifferentialEvolution — DE/rand/1/bin for single-objective real-valued problems.

Plus reusable utilities: pareto_compare, pareto_front, best_candidate, non_dominated_sort, crowding_distance, ParetoArchive, and the metrics spacing and hypervolume_2d.

Design philosophy

  • Concrete data, small trait surface. Problem, Optimizer, Initializer, Variation are the only traits a user interacts with day-to-day. Everything else is plain structs.
  • No type hell. No trait objects in the core path, no GATs, no HRTBs in user-facing APIs, no generic-RNG plumbing — Rng is a single concrete type alias.
  • Readable algorithms. Built-ins are written for clarity, not maximum abstraction reuse. RandomSearch is the recommended file to read before writing your own optimizer.
  • One crate first. No premature splitting into -core/-algorithms/ -operators. Split later if the crate grows.
  • Panic on programmer error. Invalid configuration panics with a clear message in v1; the API may grow Result-returning variants later if the base API proves useful.

See docs/heuropt_tech_design_spec.md for the full design rationale.

License

MIT.

S
Description
A practical Rust toolkit for heuristic single-, multi-, and many-objective optimization.
Readme MIT
912 KiB
Languages
Rust 100%