Files
heuropt/examples/benchmarks.rs
T
swaits a26849ed13 feat(examples): add ZDT1 and Rastrigin benchmark problems
Two canonical optimization benchmarks in a single runnable example:

- ZDT1 (Zitzler-Deb-Thiele 1): 30-D, two minimization objectives,
  closed-form Pareto front \\(f_2 = 1 - \\sqrt{f_1}\\) for
  \\(f_1 \\in [0, 1]\\). Solved with NSGA-II.
- Rastrigin: highly multimodal single-objective, global minimum
  \\(f = 0\\) at the origin. Solved with DE.

Both are public-domain mathematical formulas. Implemented as Problem
impls in examples/benchmarks.rs; main() runs each, prints front /
best, and (for ZDT1) reports the mean L2 distance from the known
analytical Pareto front so the example doubles as a sanity check on
solution quality.
2026-05-04 19:35:03 -06:00

149 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Two canonical optimization benchmarks: ZDT1 and Rastrigin.
//!
//! - **ZDT1** (Zitzler, Deb, Thiele, 2000) is the standard 2-objective
//! continuous benchmark. The analytical Pareto front is
//! `f2 = 1 - sqrt(f1)` for `f1 ∈ [0, 1]`, which we use to score the
//! solution quality of a short NSGA-II run.
//! - **Rastrigin** is the textbook multimodal single-objective trap:
//! `f(x) = 10·n + Σ (x_i² 10·cos(2π·x_i))`, global minimum `f = 0`
//! at the origin. We solve it with DE.
//!
//! Run with:
//!
//! ```bash
//! cargo run --release --example benchmarks
//! ```
//!
//! Both problems are defined by simple public-domain math and have no
//! licensing concerns.
use std::f64::consts::PI;
use heuropt::prelude::*;
/// ZDT1: 30-dimensional, two minimization objectives.
struct Zdt1 {
dim: usize,
}
impl Problem for Zdt1 {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
debug_assert_eq!(x.len(), self.dim);
// GaussianMutation does not enforce bounds (spec §11.2); clamp here so
// the example stays well-defined on a bounded benchmark. This is the
// spec-recommended pattern for handling bounds in v1.
let x0 = x[0].clamp(0.0, 1.0);
let tail_sum: f64 = x[1..].iter().map(|v| v.clamp(0.0, 1.0)).sum();
let g = 1.0 + 9.0 * tail_sum / (self.dim as f64 - 1.0);
let f1 = x0;
let f2 = g * (1.0 - (f1 / g).sqrt());
Evaluation::new(vec![f1, f2])
}
}
/// Rastrigin: n-dimensional, single minimization objective.
struct Rastrigin {
dim: usize,
}
impl Problem for Rastrigin {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let n = self.dim as f64;
let value = 10.0 * n
+ x.iter().map(|v| v * v - 10.0 * (2.0 * PI * v).cos()).sum::<f64>();
Evaluation::new(vec![value])
}
}
/// Mean L2 distance from each front point to the nearest point on the
/// analytical ZDT1 Pareto front (`f2 = 1 - sqrt(f1)` for `f1 ∈ [0, 1]`).
///
/// Since `(f1, 1 - sqrt(f1))` is monotone in `f1`, the minimum-distance
/// projection on a fine sample of the curve is good enough for a
/// human-readable quality signal.
fn mean_distance_to_zdt1_front(front: &[Candidate<Vec<f64>>]) -> f64 {
let samples: Vec<(f64, f64)> = (0..=1000)
.map(|i| {
let f1 = i as f64 / 1000.0;
(f1, 1.0 - f1.sqrt())
})
.collect();
let mut total = 0.0;
for c in front {
let f1 = c.evaluation.objectives[0];
let f2 = c.evaluation.objectives[1];
let mut best = f64::INFINITY;
for &(rf1, rf2) in &samples {
let d = ((rf1 - f1).powi(2) + (rf2 - f2).powi(2)).sqrt();
if d < best {
best = d;
}
}
total += best;
}
total / front.len().max(1) as f64
}
fn run_zdt1() {
let dim = 30;
let problem = Zdt1 { dim };
let initializer = RealBounds::new(vec![(0.0, 1.0); dim]);
let variation = GaussianMutation { sigma: 0.05 };
let config = Nsga2Config { population_size: 100, generations: 400, seed: 42 };
let mut optimizer = Nsga2::new(config, initializer, variation);
let result = optimizer.run(&problem);
let dist = mean_distance_to_zdt1_front(&result.pareto_front);
println!("== ZDT1 (NSGA-II, dim={dim}) ==");
println!(" population: {}", result.population.len());
println!(" pareto front: {}", result.pareto_front.len());
println!(" evaluations: {}", result.evaluations);
println!(" mean L2 to true front: {dist:.5}");
println!(
" example front point: f1={:.4}, f2={:.4}",
result.pareto_front[0].evaluation.objectives[0],
result.pareto_front[0].evaluation.objectives[1],
);
}
fn run_rastrigin() {
let dim = 5;
let problem = Rastrigin { dim };
let bounds = RealBounds::new(vec![(-5.12, 5.12); dim]);
let config = DifferentialEvolutionConfig {
population_size: 100,
generations: 1000,
differential_weight: 0.5,
crossover_probability: 0.9,
seed: 1,
};
let mut optimizer = DifferentialEvolution::new(config, bounds);
let result = optimizer.run(&problem);
let best = result.best.expect("single-objective always has a best");
println!("== Rastrigin (DE, dim={dim}) ==");
println!(" evaluations: {}", result.evaluations);
println!(" best f: {:.6e}", best.evaluation.objectives[0]);
println!(" best x: {:?}", best.decision);
}
fn main() {
run_zdt1();
println!();
run_rastrigin();
}