docs(examples): switch ZDT1 to canonical NSGA-II operators (SBX + PolyMut)
Replace the v0.1 `GaussianMutation` + clamp-inside-`evaluate` setup with the canonical NSGA-II operator pair: SBX (η_c=15, per-var prob 0.5) followed by PolynomialMutation (η_m=20, per-var prob 1/dim), composed via `CompositeVariation`. Both are bounds-aware on their own, so the in-evaluate clamping is dropped. Result on ZDT1 (dim=30, pop=100, gens=1000, seed=42): mean L2 distance to the analytical Pareto front is 0.00152 — comfortably within the published NSGA-II range for this benchmark. Note on the previous number: the v0.1 setup reported 0.00072 at 40k evals, but that was an artifact of clamping inside `evaluate`. Out-of- bounds Gaussian mutations on `x[0]` were snapping to 0, which coincides with the ZDT1 Pareto-front extreme (f1=0). The new operator pair has no such free lunch — it runs the actual NSGA-II algorithm — and the new measurement is what honest convergence on ZDT1 actually looks like. Generations bumped from 400 to 1000 (40k → 100k evaluations) to give the operators headroom; matches the budget DE uses for Rastrigin so the example feels balanced.
This commit is contained in:
+12
-9
@@ -35,13 +35,9 @@ impl Problem for Zdt1 {
|
||||
|
||||
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 f1 = x[0];
|
||||
let tail_sum: f64 = x[1..].iter().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])
|
||||
}
|
||||
@@ -99,9 +95,16 @@ fn mean_distance_to_zdt1_front(front: &[Candidate<Vec<f64>>]) -> 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 bounds = vec![(0.0, 1.0); dim];
|
||||
let initializer = RealBounds::new(bounds.clone());
|
||||
// Canonical NSGA-II operator pair: SBX (η_c=15) + polynomial mutation
|
||||
// (η_m=20, per-var prob 1/dim). Both are bounds-aware so children stay
|
||||
// feasible without any clamping inside `evaluate`.
|
||||
let variation = CompositeVariation {
|
||||
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
|
||||
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / dim as f64),
|
||||
};
|
||||
let config = Nsga2Config { population_size: 100, generations: 1000, seed: 42 };
|
||||
let mut optimizer = Nsga2::new(config, initializer, variation);
|
||||
|
||||
let result = optimizer.run(&problem);
|
||||
|
||||
Reference in New Issue
Block a user