feat(examples): wire SMS-EMOA, HypE, RVEA, PESA-II, ε-MOEA into compare harness

Adds runners for the five new MO algorithms in both the ZDT1 (2-obj)
and DTLZ2 (3-obj) sections of `examples/compare.rs`. The harness now
side-by-sides 11 multi-/many-objective optimizers (RandomSearch + 10
real ones) on each problem.
This commit is contained in:
2026-05-05 09:51:11 -06:00
parent 4fa8250c24
commit 3400124541
+238
View File
@@ -234,6 +234,121 @@ fn zdt1_nsga2(seed: u64) -> MoRun {
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_sms_emoa(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = vec![(0.0, 1.0); ZDT1_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT1_DIM as f64),
};
// SMS-EMOA is steady-state and computes O(N²) hypervolumes per
// iteration, so we run it on a smaller population for a smaller
// total budget to keep wall time tractable.
let config = SmsEmoaConfig {
population_size: 40,
generations: 4_000,
reference_point: vec![11.0, 11.0],
seed,
};
let mut opt = SmsEmoa::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_hype(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = vec![(0.0, 1.0); ZDT1_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT1_DIM as f64),
};
let pop = 80;
// Each generation does multiple HV-estimation passes (parent selection +
// survival truncation), each with mc_samples × N work. Keep budget
// smaller than NSGA-II's so wall time is tractable.
let gens = 80;
let config = HypeConfig {
population_size: pop,
generations: gens,
reference_point: vec![11.0, 11.0],
mc_samples: 1_000,
seed,
};
let mut opt = Hype::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_rvea(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = vec![(0.0, 1.0); ZDT1_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT1_DIM as f64),
};
let pop = 100;
let gens = ZDT1_BUDGET / pop;
let config = RveaConfig {
population_size: pop,
generations: gens,
reference_divisions: 99,
alpha: 2.0,
seed,
};
let mut opt = Rvea::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_pesa2(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = vec![(0.0, 1.0); ZDT1_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT1_DIM as f64),
};
let pop = 50;
let gens = (ZDT1_BUDGET - pop) / pop;
let config = PesaIIConfig {
population_size: pop,
archive_size: 100,
generations: gens,
grid_divisions: 20,
seed,
};
let mut opt = PesaII::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_epsilon_moea(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = vec![(0.0, 1.0); ZDT1_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT1_DIM as f64),
};
let config = EpsilonMoeaConfig {
population_size: 50,
evaluations: ZDT1_BUDGET,
epsilon: vec![0.01, 0.01],
seed,
};
let mut opt = EpsilonMoea::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn zdt1_mopso(seed: u64) -> MoRun {
let problem = Zdt1 { dim: ZDT1_DIM };
let bounds = RealBounds::new(vec![(0.0, 1.0); ZDT1_DIM]);
@@ -380,6 +495,119 @@ fn dtlz2_spea2(seed: u64) -> MoRun {
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_sms_emoa(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = vec![(0.0, 1.0); DTLZ2_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 30.0, 1.0),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ2_DIM as f64),
};
// 3-D HV-by-slicing is significantly more expensive than 2-D; smaller
// pop and gen budget here to keep the comparison tractable.
let pop = 40;
let gens = 4_000;
let config = SmsEmoaConfig {
population_size: pop,
generations: gens,
reference_point: vec![3.0, 3.0, 3.0],
seed,
};
let mut opt = SmsEmoa::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_hype(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = vec![(0.0, 1.0); DTLZ2_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 30.0, 1.0),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ2_DIM as f64),
};
let pop = 80;
let gens = 80;
let config = HypeConfig {
population_size: pop,
generations: gens,
reference_point: vec![3.0, 3.0, 3.0],
mc_samples: 1_000,
seed,
};
let mut opt = Hype::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_rvea(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = vec![(0.0, 1.0); DTLZ2_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 30.0, 1.0),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ2_DIM as f64),
};
let pop = 92;
let gens = DTLZ2_BUDGET / pop;
let config = RveaConfig {
population_size: pop,
generations: gens,
reference_divisions: 12,
alpha: 2.0,
seed,
};
let mut opt = Rvea::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_pesa2(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = vec![(0.0, 1.0); DTLZ2_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 30.0, 1.0),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ2_DIM as f64),
};
let pop = 50;
let gens = (DTLZ2_BUDGET - pop) / pop;
let config = PesaIIConfig {
population_size: pop,
archive_size: 100,
generations: gens,
grid_divisions: 12,
seed,
};
let mut opt = PesaII::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_epsilon_moea(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = vec![(0.0, 1.0); DTLZ2_DIM];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 30.0, 1.0),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ2_DIM as f64),
};
let config = EpsilonMoeaConfig {
population_size: 50,
evaluations: DTLZ2_BUDGET,
epsilon: vec![0.05, 0.05, 0.05],
seed,
};
let mut opt = EpsilonMoea::new(config, initializer, variation);
let t0 = Instant::now();
let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() }
}
fn dtlz2_mopso(seed: u64) -> MoRun {
let problem = dtlz2_problem();
let bounds = RealBounds::new(vec![(0.0, 1.0); DTLZ2_DIM]);
@@ -689,7 +917,12 @@ fn run_zdt1_comparison() {
("PAES", zdt1_paes),
("MOPSO", zdt1_mopso),
("SPEA2", zdt1_spea2),
("PESA-II", zdt1_pesa2),
("ε-MOEA", zdt1_epsilon_moea),
("IBEA", zdt1_ibea),
("HypE", zdt1_hype),
("SMS-EMOA", zdt1_sms_emoa),
("RVEA", zdt1_rvea),
("NSGA-II", zdt1_nsga2),
("NSGA-III", zdt1_nsga3),
("MOEA/D", zdt1_moead),
@@ -748,7 +981,12 @@ fn run_dtlz2_comparison() {
("MOPSO", dtlz2_mopso),
("NSGA-II", dtlz2_nsga2),
("SPEA2", dtlz2_spea2),
("PESA-II", dtlz2_pesa2),
("ε-MOEA", dtlz2_epsilon_moea),
("IBEA", dtlz2_ibea),
("HypE", dtlz2_hype),
("SMS-EMOA", dtlz2_sms_emoa),
("RVEA", dtlz2_rvea),
("NSGA-III", dtlz2_nsga3),
("MOEA/D", dtlz2_moead),
];