parallel feature for population-evaluation parallelism
Adds a `parallel` Cargo feature that pulls in rayon and parallelizes the only step that's actually expensive in practice — calls to `Problem::evaluate` — across the population. RNG-driven steps (parent and donor selection, variation, replacement decisions) stay serial, so seeded runs remain deterministic regardless of feature state, and the default and `--features parallel` builds produce bit-identical results. Wiring: - New `algorithms::parallel_eval::evaluate_batch` helper with two cfg-gated implementations (rayon's `into_par_iter` when the feature is on, plain `into_iter` otherwise). Both preserve input order, so pareto_front and crowding-distance decisions remain reproducible. - `RandomSearch`, `Nsga2`, and `DifferentialEvolution` now route population/offspring evaluation through the helper. NSGA-II's main loop is restructured into a serial selection-and-variation phase followed by a parallel-friendly batch evaluation phase. - DE's per-target loop is restructured into three phases (serial trial construction → batch evaluation → serial replacement). Side effect of the restructuring: DE is now the canonical synchronous DE/rand/1/bin rather than the asynchronous variant where target `i+1` sees `i`'s in-flight update. Synchronous is the textbook formulation, so this is a small correctness improvement on top of the parallelism enable. - PAES stays serial — its main loop has a sequential dependency on the current candidate and would gain nothing from rayon. Cost: algorithm impls now require `P: Sync` and `P::Decision: Send` unconditionally so a single impl serves both feature modes. This is a small bound tightening that any plain-data Problem already satisfies; in return the public `Problem` trait itself stays unchanged and the default build picks up no new dependencies. Verified: - `cargo test` and `cargo test --features parallel` both pass; the Nsga2 `deterministic_with_same_seed` test confirms reproducibility. - `cargo run --release --example benchmarks` and the same with `--features parallel` produce bit-identical ZDT1 / Rastrigin results.
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: derive serde::{Serialize, Deserialize} on the core data types.
# heuropt = { version = "0.1", features = ["serde"] }
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,Variationare 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 —
Rngis a single concrete type alias. - Readable algorithms. Built-ins are written for clarity, not maximum
abstraction reuse.
RandomSearchis 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.