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.
This commit is contained in:
2026-05-04 19:35:03 -06:00
parent e3f5d3eb7b
commit a26849ed13
+148
View File
@@ -0,0 +1,148 @@
//! 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();
}