style: apply rustfmt drift across the crate

This commit is contained in:
2026-05-05 11:40:14 -06:00
parent 84cee3f29e
commit 4a59041d1a
60 changed files with 1520 additions and 608 deletions
+1
View File
@@ -0,0 +1 @@
{"sessionId":"ac44d107-52ca-4cd4-9586-ae2fe91bc9f7","pid":2366937,"procStart":"77336928","acquiredAt":1778002505967}
+122 -37
View File
@@ -14,10 +14,10 @@ use gungraun::prelude::*;
use heuropt::core::candidate::Candidate; use heuropt::core::candidate::Candidate;
use heuropt::core::evaluation::Evaluation; use heuropt::core::evaluation::Evaluation;
use heuropt::core::objective::{Objective, ObjectiveSpace}; use heuropt::core::objective::{Objective, ObjectiveSpace};
use heuropt::core::problem::Problem;
use heuropt::metrics::hypervolume::{hypervolume_2d, hypervolume_nd}; use heuropt::metrics::hypervolume::{hypervolume_2d, hypervolume_nd};
use heuropt::pareto::crowding::crowding_distance; use heuropt::pareto::crowding::crowding_distance;
use heuropt::pareto::sort::non_dominated_sort; use heuropt::pareto::sort::non_dominated_sort;
use heuropt::core::problem::Problem;
use heuropt::prelude::*; use heuropt::prelude::*;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -53,7 +53,11 @@ fn crowding_distance_2d(n: usize) -> Vec<f64> {
let pop = make_2d_population(n); let pop = make_2d_population(n);
let s = space_2d(); let s = space_2d();
let front: Vec<usize> = (0..pop.len()).collect(); let front: Vec<usize> = (0..pop.len()).collect();
black_box(crowding_distance(black_box(&pop), black_box(&front), black_box(&s))) black_box(crowding_distance(
black_box(&pop),
black_box(&front),
black_box(&s),
))
} }
#[library_benchmark] #[library_benchmark]
@@ -62,7 +66,11 @@ fn crowding_distance_2d(n: usize) -> Vec<f64> {
fn hypervolume_2d_bench(n: usize) -> f64 { fn hypervolume_2d_bench(n: usize) -> f64 {
let pop = make_2d_population(n); let pop = make_2d_population(n);
let s = space_2d(); let s = space_2d();
black_box(hypervolume_2d(black_box(&pop), black_box(&s), black_box([1.1, 1.1]))) black_box(hypervolume_2d(
black_box(&pop),
black_box(&s),
black_box([1.1, 1.1]),
))
} }
fn make_3d_population(n: usize) -> (Vec<Candidate<()>>, ObjectiveSpace) { fn make_3d_population(n: usize) -> (Vec<Candidate<()>>, ObjectiveSpace) {
@@ -75,10 +83,7 @@ fn make_3d_population(n: usize) -> (Vec<Candidate<()>>, ObjectiveSpace) {
.map(|i| { .map(|i| {
let t = i as f64 / n as f64; let t = i as f64 / n as f64;
let theta = 0.5 * std::f64::consts::PI * t; let theta = 0.5 * std::f64::consts::PI * t;
Candidate::new( Candidate::new((), Evaluation::new(vec![theta.cos(), theta.sin(), 1.0 - t]))
(),
Evaluation::new(vec![theta.cos(), theta.sin(), 1.0 - t]),
)
}) })
.collect(); .collect();
(pop, s) (pop, s)
@@ -89,7 +94,11 @@ fn make_3d_population(n: usize) -> (Vec<Candidate<()>>, ObjectiveSpace) {
#[bench::n_100(100)] #[bench::n_100(100)]
fn hypervolume_nd_bench_3d(n: usize) -> f64 { fn hypervolume_nd_bench_3d(n: usize) -> f64 {
let (pop, s) = make_3d_population(n); let (pop, s) = make_3d_population(n);
black_box(hypervolume_nd(black_box(&pop), black_box(&s), black_box(&[2.0, 2.0, 2.0]))) black_box(hypervolume_nd(
black_box(&pop),
black_box(&s),
black_box(&[2.0, 2.0, 2.0]),
))
} }
library_benchmark_group!( library_benchmark_group!(
@@ -128,7 +137,11 @@ fn nsga2_one_generation() -> usize {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
let mut opt = Nsga2::new( let mut opt = Nsga2::new(
Nsga2Config { population_size: 50, generations: 1, seed: 0 }, Nsga2Config {
population_size: 50,
generations: 1,
seed: 0,
},
initializer, initializer,
variation, variation,
); );
@@ -191,7 +204,11 @@ fn so_bounds() -> RealBounds {
#[library_benchmark] #[library_benchmark]
fn random_search_short() -> usize { fn random_search_short() -> usize {
let mut o = RandomSearch::new( let mut o = RandomSearch::new(
RandomSearchConfig { iterations: 50, batch_size: 1, seed: 0 }, RandomSearchConfig {
iterations: 50,
batch_size: 1,
seed: 0,
},
so_bounds(), so_bounds(),
); );
black_box(o.run(black_box(&Sphere1D)).evaluations) black_box(o.run(black_box(&Sphere1D)).evaluations)
@@ -200,7 +217,10 @@ fn random_search_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn hill_climber_short() -> usize { fn hill_climber_short() -> usize {
let mut o = HillClimber::new( let mut o = HillClimber::new(
HillClimberConfig { iterations: 50, seed: 0 }, HillClimberConfig {
iterations: 50,
seed: 0,
},
so_bounds(), so_bounds(),
GaussianMutation { sigma: 0.1 }, GaussianMutation { sigma: 0.1 },
); );
@@ -291,7 +311,11 @@ fn differential_evolution_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn tlbo_short() -> usize { fn tlbo_short() -> usize {
let mut o = Tlbo::new( let mut o = Tlbo::new(
TlboConfig { population_size: 10, generations: 5, seed: 0 }, TlboConfig {
population_size: 10,
generations: 5,
seed: 0,
},
so_bounds(), so_bounds(),
); );
black_box(o.run(black_box(&Sphere1D)).evaluations) black_box(o.run(black_box(&Sphere1D)).evaluations)
@@ -316,7 +340,10 @@ fn separable_nes_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn nelder_mead_short() -> usize { fn nelder_mead_short() -> usize {
let mut o = NelderMead::new( let mut o = NelderMead::new(
NelderMeadConfig { iterations: 50, ..NelderMeadConfig::default() }, NelderMeadConfig {
iterations: 50,
..NelderMeadConfig::default()
},
so_bounds(), so_bounds(),
); );
black_box(o.run(black_box(&Sphere1D)).evaluations) black_box(o.run(black_box(&Sphere1D)).evaluations)
@@ -388,8 +415,7 @@ library_benchmark_group!(
fn schaffer_bounds() -> Vec<(f64, f64)> { fn schaffer_bounds() -> Vec<(f64, f64)> {
vec![(-3.0, 3.0)] vec![(-3.0, 3.0)]
} }
fn mo_variation() fn mo_variation() -> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
-> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
let bounds = schaffer_bounds(); let bounds = schaffer_bounds();
CompositeVariation { CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
@@ -400,7 +426,12 @@ fn mo_variation()
#[library_benchmark] #[library_benchmark]
fn nsga3_short() -> usize { fn nsga3_short() -> usize {
let mut o = Nsga3::new( let mut o = Nsga3::new(
Nsga3Config { population_size: 12, generations: 1, reference_divisions: 11, seed: 0 }, Nsga3Config {
population_size: 12,
generations: 1,
reference_divisions: 11,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -410,7 +441,12 @@ fn nsga3_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn spea2_short() -> usize { fn spea2_short() -> usize {
let mut o = Spea2::new( let mut o = Spea2::new(
Spea2Config { population_size: 10, archive_size: 10, generations: 1, seed: 0 }, Spea2Config {
population_size: 10,
archive_size: 10,
generations: 1,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -420,7 +456,12 @@ fn spea2_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn moead_short() -> usize { fn moead_short() -> usize {
let mut o = Moead::new( let mut o = Moead::new(
MoeadConfig { generations: 1, reference_divisions: 9, neighborhood_size: 4, seed: 0 }, MoeadConfig {
generations: 1,
reference_divisions: 9,
neighborhood_size: 4,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -431,8 +472,13 @@ fn moead_short() -> usize {
fn mopso_short() -> usize { fn mopso_short() -> usize {
let mut o = Mopso::new( let mut o = Mopso::new(
MopsoConfig { MopsoConfig {
swarm_size: 10, generations: 1, archive_size: 10, swarm_size: 10,
inertia: 0.7, cognitive: 1.5, social: 1.5, seed: 0, generations: 1,
archive_size: 10,
inertia: 0.7,
cognitive: 1.5,
social: 1.5,
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
); );
@@ -442,7 +488,12 @@ fn mopso_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn ibea_short() -> usize { fn ibea_short() -> usize {
let mut o = Ibea::new( let mut o = Ibea::new(
IbeaConfig { population_size: 10, generations: 1, kappa: 0.05, seed: 0 }, IbeaConfig {
population_size: 10,
generations: 1,
kappa: 0.05,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -453,8 +504,10 @@ fn ibea_short() -> usize {
fn sms_emoa_short() -> usize { fn sms_emoa_short() -> usize {
let mut o = SmsEmoa::new( let mut o = SmsEmoa::new(
SmsEmoaConfig { SmsEmoaConfig {
population_size: 8, generations: 5, population_size: 8,
reference_point: vec![10.0, 10.0], seed: 0, generations: 5,
reference_point: vec![10.0, 10.0],
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
@@ -466,8 +519,11 @@ fn sms_emoa_short() -> usize {
fn hype_short() -> usize { fn hype_short() -> usize {
let mut o = Hype::new( let mut o = Hype::new(
HypeConfig { HypeConfig {
population_size: 10, generations: 1, population_size: 10,
reference_point: vec![10.0, 10.0], mc_samples: 100, seed: 0, generations: 1,
reference_point: vec![10.0, 10.0],
mc_samples: 100,
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
@@ -479,8 +535,11 @@ fn hype_short() -> usize {
fn pesa2_short() -> usize { fn pesa2_short() -> usize {
let mut o = PesaII::new( let mut o = PesaII::new(
PesaIIConfig { PesaIIConfig {
population_size: 10, archive_size: 10, generations: 1, population_size: 10,
grid_divisions: 4, seed: 0, archive_size: 10,
generations: 1,
grid_divisions: 4,
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
@@ -492,8 +551,10 @@ fn pesa2_short() -> usize {
fn epsilon_moea_short() -> usize { fn epsilon_moea_short() -> usize {
let mut o = EpsilonMoea::new( let mut o = EpsilonMoea::new(
EpsilonMoeaConfig { EpsilonMoeaConfig {
population_size: 10, evaluations: 30, population_size: 10,
epsilon: vec![0.05, 0.05], seed: 0, evaluations: 30,
epsilon: vec![0.05, 0.05],
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
@@ -504,7 +565,11 @@ fn epsilon_moea_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn age_moea_short() -> usize { fn age_moea_short() -> usize {
let mut o = AgeMoea::new( let mut o = AgeMoea::new(
AgeMoeaConfig { population_size: 10, generations: 1, seed: 0 }, AgeMoeaConfig {
population_size: 10,
generations: 1,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -514,7 +579,12 @@ fn age_moea_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn grea_short() -> usize { fn grea_short() -> usize {
let mut o = Grea::new( let mut o = Grea::new(
GreaConfig { population_size: 10, generations: 1, grid_divisions: 4, seed: 0 }, GreaConfig {
population_size: 10,
generations: 1,
grid_divisions: 4,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -524,7 +594,11 @@ fn grea_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn knea_short() -> usize { fn knea_short() -> usize {
let mut o = Knea::new( let mut o = Knea::new(
KneaConfig { population_size: 10, generations: 1, seed: 0 }, KneaConfig {
population_size: 10,
generations: 1,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
); );
@@ -535,8 +609,11 @@ fn knea_short() -> usize {
fn rvea_short() -> usize { fn rvea_short() -> usize {
let mut o = Rvea::new( let mut o = Rvea::new(
RveaConfig { RveaConfig {
population_size: 10, generations: 1, population_size: 10,
reference_divisions: 9, alpha: 2.0, seed: 0, generations: 1,
reference_divisions: 9,
alpha: 2.0,
seed: 0,
}, },
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
mo_variation(), mo_variation(),
@@ -547,7 +624,11 @@ fn rvea_short() -> usize {
#[library_benchmark] #[library_benchmark]
fn paes_short() -> usize { fn paes_short() -> usize {
let mut o = Paes::new( let mut o = Paes::new(
PaesConfig { iterations: 30, archive_size: 10, seed: 0 }, PaesConfig {
iterations: 30,
archive_size: 10,
seed: 0,
},
RealBounds::new(schaffer_bounds()), RealBounds::new(schaffer_bounds()),
GaussianMutation { sigma: 0.1 }, GaussianMutation { sigma: 0.1 },
); );
@@ -562,5 +643,9 @@ library_benchmark_group!(
age_moea_short, grea_short, knea_short, rvea_short, paes_short age_moea_short, grea_short, knea_short, rvea_short, paes_short
); );
main!(library_benchmark_groups = main!(
pareto_group, algorithm_group, single_objective_group, multi_objective_group); library_benchmark_groups = pareto_group,
algorithm_group,
single_objective_group,
multi_objective_group
);
+8 -2
View File
@@ -58,7 +58,9 @@ impl Problem for Rastrigin {
fn evaluate(&self, x: &Vec<f64>) -> Evaluation { fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let n = self.dim as f64; let n = self.dim as f64;
let value = 10.0 * n let value = 10.0 * n
+ x.iter().map(|v| v * v - 10.0 * (2.0 * PI * v).cos()).sum::<f64>(); + x.iter()
.map(|v| v * v - 10.0 * (2.0 * PI * v).cos())
.sum::<f64>();
Evaluation::new(vec![value]) Evaluation::new(vec![value])
} }
} }
@@ -104,7 +106,11 @@ fn run_zdt1() {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / dim as f64), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / dim as f64),
}; };
let config = Nsga2Config { population_size: 100, generations: 1000, seed: 42 }; let config = Nsga2Config {
population_size: 100,
generations: 1000,
seed: 42,
};
let mut optimizer = Nsga2::new(config, initializer, variation); let mut optimizer = Nsga2::new(config, initializer, variation);
let result = optimizer.run(&problem); let result = optimizer.run(&problem);
+319 -80
View File
@@ -145,8 +145,7 @@ impl Problem for Ackley {
let n = self.dim as f64; let n = self.dim as f64;
let sum_sq: f64 = x.iter().map(|v| v * v).sum(); let sum_sq: f64 = x.iter().map(|v| v * v).sum();
let sum_cos: f64 = x.iter().map(|v| (2.0 * PI * v).cos()).sum(); let sum_cos: f64 = x.iter().map(|v| (2.0 * PI * v).cos()).sum();
let f = -20.0 * (-0.2 * (sum_sq / n).sqrt()).exp() let f = -20.0 * (-0.2 * (sum_sq / n).sqrt()).exp() - (sum_cos / n).exp()
- (sum_cos / n).exp()
+ 20.0 + 20.0
+ std::f64::consts::E; + std::f64::consts::E;
Evaluation::new(vec![f]) Evaluation::new(vec![f])
@@ -228,7 +227,9 @@ impl Problem for Rastrigin {
fn evaluate(&self, x: &Vec<f64>) -> Evaluation { fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let n = self.dim as f64; let n = self.dim as f64;
let value = 10.0 * n let value = 10.0 * n
+ x.iter().map(|v| v * v - 10.0 * (2.0 * PI * v).cos()).sum::<f64>(); + x.iter()
.map(|v| v * v - 10.0 * (2.0 * PI * v).cos())
.sum::<f64>();
Evaluation::new(vec![value]) Evaluation::new(vec![value])
} }
} }
@@ -297,7 +298,10 @@ fn zdt1_random(seed: u64) -> MoRun {
let mut opt = RandomSearch::new(config, initializer); let mut opt = RandomSearch::new(config, initializer);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_paes(seed: u64) -> MoRun { fn zdt1_paes(seed: u64) -> MoRun {
@@ -312,7 +316,10 @@ fn zdt1_paes(seed: u64) -> MoRun {
let mut opt = Paes::new(config, initializer, variation); let mut opt = Paes::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_spea2(seed: u64) -> MoRun { fn zdt1_spea2(seed: u64) -> MoRun {
@@ -336,7 +343,10 @@ fn zdt1_spea2(seed: u64) -> MoRun {
let mut opt = Spea2::new(config, initializer, variation); let mut opt = Spea2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_nsga2(seed: u64) -> MoRun { fn zdt1_nsga2(seed: u64) -> MoRun {
@@ -349,11 +359,18 @@ fn zdt1_nsga2(seed: u64) -> MoRun {
}; };
let pop = 100; let pop = 100;
let gens = ZDT1_BUDGET / pop; let gens = ZDT1_BUDGET / pop;
let config = Nsga2Config { population_size: pop, generations: gens, seed }; let config = Nsga2Config {
population_size: pop,
generations: gens,
seed,
};
let mut opt = Nsga2::new(config, initializer, variation); let mut opt = Nsga2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_sms_emoa(seed: u64) -> MoRun { fn zdt1_sms_emoa(seed: u64) -> MoRun {
@@ -376,7 +393,10 @@ fn zdt1_sms_emoa(seed: u64) -> MoRun {
let mut opt = SmsEmoa::new(config, initializer, variation); let mut opt = SmsEmoa::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_hype(seed: u64) -> MoRun { fn zdt1_hype(seed: u64) -> MoRun {
@@ -402,7 +422,10 @@ fn zdt1_hype(seed: u64) -> MoRun {
let mut opt = Hype::new(config, initializer, variation); let mut opt = Hype::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_rvea(seed: u64) -> MoRun { fn zdt1_rvea(seed: u64) -> MoRun {
@@ -425,7 +448,10 @@ fn zdt1_rvea(seed: u64) -> MoRun {
let mut opt = Rvea::new(config, initializer, variation); let mut opt = Rvea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_pesa2(seed: u64) -> MoRun { fn zdt1_pesa2(seed: u64) -> MoRun {
@@ -448,7 +474,10 @@ fn zdt1_pesa2(seed: u64) -> MoRun {
let mut opt = PesaII::new(config, initializer, variation); let mut opt = PesaII::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_epsilon_moea(seed: u64) -> MoRun { fn zdt1_epsilon_moea(seed: u64) -> MoRun {
@@ -468,7 +497,10 @@ fn zdt1_epsilon_moea(seed: u64) -> MoRun {
let mut opt = EpsilonMoea::new(config, initializer, variation); let mut opt = EpsilonMoea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_mopso(seed: u64) -> MoRun { fn zdt1_mopso(seed: u64) -> MoRun {
@@ -488,7 +520,10 @@ fn zdt1_mopso(seed: u64) -> MoRun {
let mut opt = Mopso::new(config, bounds); let mut opt = Mopso::new(config, bounds);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_ibea(seed: u64) -> MoRun { fn zdt1_ibea(seed: u64) -> MoRun {
@@ -501,11 +536,19 @@ fn zdt1_ibea(seed: u64) -> MoRun {
}; };
let pop = 100; let pop = 100;
let gens = ZDT1_BUDGET / pop; let gens = ZDT1_BUDGET / pop;
let config = IbeaConfig { population_size: pop, generations: gens, kappa: 0.05, seed }; let config = IbeaConfig {
population_size: pop,
generations: gens,
kappa: 0.05,
seed,
};
let mut opt = Ibea::new(config, initializer, variation); let mut opt = Ibea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_moead(seed: u64) -> MoRun { fn zdt1_moead(seed: u64) -> MoRun {
@@ -529,7 +572,10 @@ fn zdt1_moead(seed: u64) -> MoRun {
let mut opt = Moead::new(config, initializer, variation); let mut opt = Moead::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt1_nsga3(seed: u64) -> MoRun { fn zdt1_nsga3(seed: u64) -> MoRun {
@@ -552,7 +598,10 @@ fn zdt1_nsga3(seed: u64) -> MoRun {
let mut opt = Nsga3::new(config, initializer, variation); let mut opt = Nsga3::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -560,7 +609,10 @@ fn zdt1_nsga3(seed: u64) -> MoRun {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
fn dtlz2_problem() -> Dtlz2 { fn dtlz2_problem() -> Dtlz2 {
Dtlz2 { num_objectives: DTLZ2_OBJECTIVES, dim: DTLZ2_DIM } Dtlz2 {
num_objectives: DTLZ2_OBJECTIVES,
dim: DTLZ2_DIM,
}
} }
fn dtlz2_random(seed: u64) -> MoRun { fn dtlz2_random(seed: u64) -> MoRun {
@@ -574,7 +626,10 @@ fn dtlz2_random(seed: u64) -> MoRun {
let mut opt = RandomSearch::new(config, initializer); let mut opt = RandomSearch::new(config, initializer);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_nsga2(seed: u64) -> MoRun { fn dtlz2_nsga2(seed: u64) -> MoRun {
@@ -587,11 +642,18 @@ fn dtlz2_nsga2(seed: u64) -> MoRun {
}; };
let pop = 92; // close to the 91-ref-point NSGA-III pop, for fairness let pop = 92; // close to the 91-ref-point NSGA-III pop, for fairness
let gens = DTLZ2_BUDGET / pop; let gens = DTLZ2_BUDGET / pop;
let config = Nsga2Config { population_size: pop, generations: gens, seed }; let config = Nsga2Config {
population_size: pop,
generations: gens,
seed,
};
let mut opt = Nsga2::new(config, initializer, variation); let mut opt = Nsga2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_spea2(seed: u64) -> MoRun { fn dtlz2_spea2(seed: u64) -> MoRun {
@@ -614,7 +676,10 @@ fn dtlz2_spea2(seed: u64) -> MoRun {
let mut opt = Spea2::new(config, initializer, variation); let mut opt = Spea2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_sms_emoa(seed: u64) -> MoRun { fn dtlz2_sms_emoa(seed: u64) -> MoRun {
@@ -638,7 +703,10 @@ fn dtlz2_sms_emoa(seed: u64) -> MoRun {
let mut opt = SmsEmoa::new(config, initializer, variation); let mut opt = SmsEmoa::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_hype(seed: u64) -> MoRun { fn dtlz2_hype(seed: u64) -> MoRun {
@@ -661,7 +729,10 @@ fn dtlz2_hype(seed: u64) -> MoRun {
let mut opt = Hype::new(config, initializer, variation); let mut opt = Hype::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_rvea(seed: u64) -> MoRun { fn dtlz2_rvea(seed: u64) -> MoRun {
@@ -684,7 +755,10 @@ fn dtlz2_rvea(seed: u64) -> MoRun {
let mut opt = Rvea::new(config, initializer, variation); let mut opt = Rvea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_pesa2(seed: u64) -> MoRun { fn dtlz2_pesa2(seed: u64) -> MoRun {
@@ -707,7 +781,10 @@ fn dtlz2_pesa2(seed: u64) -> MoRun {
let mut opt = PesaII::new(config, initializer, variation); let mut opt = PesaII::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_epsilon_moea(seed: u64) -> MoRun { fn dtlz2_epsilon_moea(seed: u64) -> MoRun {
@@ -727,7 +804,10 @@ fn dtlz2_epsilon_moea(seed: u64) -> MoRun {
let mut opt = EpsilonMoea::new(config, initializer, variation); let mut opt = EpsilonMoea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_mopso(seed: u64) -> MoRun { fn dtlz2_mopso(seed: u64) -> MoRun {
@@ -747,7 +827,10 @@ fn dtlz2_mopso(seed: u64) -> MoRun {
let mut opt = Mopso::new(config, bounds); let mut opt = Mopso::new(config, bounds);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_ibea(seed: u64) -> MoRun { fn dtlz2_ibea(seed: u64) -> MoRun {
@@ -760,11 +843,19 @@ fn dtlz2_ibea(seed: u64) -> MoRun {
}; };
let pop = 92; let pop = 92;
let gens = DTLZ2_BUDGET / pop; let gens = DTLZ2_BUDGET / pop;
let config = IbeaConfig { population_size: pop, generations: gens, kappa: 0.05, seed }; let config = IbeaConfig {
population_size: pop,
generations: gens,
kappa: 0.05,
seed,
};
let mut opt = Ibea::new(config, initializer, variation); let mut opt = Ibea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_moead(seed: u64) -> MoRun { fn dtlz2_moead(seed: u64) -> MoRun {
@@ -787,7 +878,10 @@ fn dtlz2_moead(seed: u64) -> MoRun {
let mut opt = Moead::new(config, initializer, variation); let mut opt = Moead::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz2_nsga3(seed: u64) -> MoRun { fn dtlz2_nsga3(seed: u64) -> MoRun {
@@ -811,7 +905,10 @@ fn dtlz2_nsga3(seed: u64) -> MoRun {
let mut opt = Nsga3::new(config, initializer, variation); let mut opt = Nsga3::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
/// DTLZ2's analytical Pareto front is the unit sphere octant in objective /// DTLZ2's analytical Pareto front is the unit sphere octant in objective
@@ -824,7 +921,13 @@ fn mean_distance_to_dtlz2_front(front: &[Candidate<Vec<f64>>]) -> f64 {
let total: f64 = front let total: f64 = front
.iter() .iter()
.map(|c| { .map(|c| {
let norm: f64 = c.evaluation.objectives.iter().map(|v| v * v).sum::<f64>().sqrt(); let norm: f64 = c
.evaluation
.objectives
.iter()
.map(|v| v * v)
.sum::<f64>()
.sqrt();
(norm - 1.0).abs() (norm - 1.0).abs()
}) })
.sum(); .sum();
@@ -880,7 +983,11 @@ fn rastrigin_nsga2(seed: u64) -> SoRun {
}; };
let pop = 50; let pop = 50;
let gens = RASTRIGIN_BUDGET / pop; let gens = RASTRIGIN_BUDGET / pop;
let config = Nsga2Config { population_size: pop, generations: gens, seed }; let config = Nsga2Config {
population_size: pop,
generations: gens,
seed,
};
let mut opt = Nsga2::new(config, initializer, variation); let mut opt = Nsga2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
@@ -915,7 +1022,10 @@ fn rastrigin_hill_climber(seed: u64) -> SoRun {
let problem = Rastrigin { dim: RASTRIGIN_DIM }; let problem = Rastrigin { dim: RASTRIGIN_DIM };
let initializer = RealBounds::new(vec![(-5.12, 5.12); RASTRIGIN_DIM]); let initializer = RealBounds::new(vec![(-5.12, 5.12); RASTRIGIN_DIM]);
let variation = BoundedGaussianMutation::new(0.3, vec![(-5.12, 5.12); RASTRIGIN_DIM]); let variation = BoundedGaussianMutation::new(0.3, vec![(-5.12, 5.12); RASTRIGIN_DIM]);
let config = HillClimberConfig { iterations: RASTRIGIN_BUDGET, seed }; let config = HillClimberConfig {
iterations: RASTRIGIN_BUDGET,
seed,
};
let mut opt = HillClimber::new(config, initializer, variation); let mut opt = HillClimber::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
@@ -1137,7 +1247,9 @@ fn ackley_bo(seed: u64) -> SoRun {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
fn rosenbrock_problem() -> Rosenbrock { fn rosenbrock_problem() -> Rosenbrock {
Rosenbrock { dim: ROSENBROCK_DIM } Rosenbrock {
dim: ROSENBROCK_DIM,
}
} }
fn ackley_problem() -> Ackley { fn ackley_problem() -> Ackley {
Ackley { dim: ACKLEY_DIM } Ackley { dim: ACKLEY_DIM }
@@ -1176,7 +1288,7 @@ macro_rules! so_run_cma {
generations: $budget / pop, generations: $budget / pop,
initial_sigma: 1.0, initial_sigma: 1.0,
eigen_decomposition_period: 1, eigen_decomposition_period: 1,
initial_mean: None, initial_mean: None,
seed: $seed, seed: $seed,
}; };
let mut opt = CmaEs::new(config, bounds); let mut opt = CmaEs::new(config, bounds);
@@ -1219,7 +1331,11 @@ macro_rules! so_run_tlbo {
let pop = 30; let pop = 30;
// TLBO does ~2N evaluations per generation. // TLBO does ~2N evaluations per generation.
let gens = ($budget - pop) / (2 * pop); let gens = ($budget - pop) / (2 * pop);
let config = TlboConfig { population_size: pop, generations: gens, seed: $seed }; let config = TlboConfig {
population_size: pop,
generations: gens,
seed: $seed,
};
let mut opt = Tlbo::new(config, bounds); let mut opt = Tlbo::new(config, bounds);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
@@ -1230,21 +1346,95 @@ macro_rules! so_run_tlbo {
}}; }};
} }
fn rosenbrock_de(seed: u64) -> SoRun { so_run_de!(rosenbrock_problem(), ROSENBROCK_DIM, -5.0, 10.0, ROSENBROCK_BUDGET, seed) } fn rosenbrock_de(seed: u64) -> SoRun {
fn rosenbrock_cma(seed: u64) -> SoRun { so_run_cma!(rosenbrock_problem(), ROSENBROCK_DIM, -5.0, 10.0, ROSENBROCK_BUDGET, seed) } so_run_de!(
fn rosenbrock_pso(seed: u64) -> SoRun { so_run_pso!(rosenbrock_problem(), ROSENBROCK_DIM, -5.0, 10.0, ROSENBROCK_BUDGET, seed) } rosenbrock_problem(),
fn rosenbrock_tlbo(seed: u64) -> SoRun { so_run_tlbo!(rosenbrock_problem(), ROSENBROCK_DIM, -5.0, 10.0, ROSENBROCK_BUDGET, seed) } ROSENBROCK_DIM,
-5.0,
10.0,
ROSENBROCK_BUDGET,
seed
)
}
fn rosenbrock_cma(seed: u64) -> SoRun {
so_run_cma!(
rosenbrock_problem(),
ROSENBROCK_DIM,
-5.0,
10.0,
ROSENBROCK_BUDGET,
seed
)
}
fn rosenbrock_pso(seed: u64) -> SoRun {
so_run_pso!(
rosenbrock_problem(),
ROSENBROCK_DIM,
-5.0,
10.0,
ROSENBROCK_BUDGET,
seed
)
}
fn rosenbrock_tlbo(seed: u64) -> SoRun {
so_run_tlbo!(
rosenbrock_problem(),
ROSENBROCK_DIM,
-5.0,
10.0,
ROSENBROCK_BUDGET,
seed
)
}
fn ackley_de(seed: u64) -> SoRun { so_run_de!(ackley_problem(), ACKLEY_DIM, -32.768, 32.768, ACKLEY_BUDGET, seed) } fn ackley_de(seed: u64) -> SoRun {
fn ackley_cma(seed: u64) -> SoRun { so_run_cma!(ackley_problem(), ACKLEY_DIM, -32.768, 32.768, ACKLEY_BUDGET, seed) } so_run_de!(
fn ackley_pso(seed: u64) -> SoRun { so_run_pso!(ackley_problem(), ACKLEY_DIM, -32.768, 32.768, ACKLEY_BUDGET, seed) } ackley_problem(),
fn ackley_tlbo(seed: u64) -> SoRun { so_run_tlbo!(ackley_problem(), ACKLEY_DIM, -32.768, 32.768, ACKLEY_BUDGET, seed) } ACKLEY_DIM,
-32.768,
32.768,
ACKLEY_BUDGET,
seed
)
}
fn ackley_cma(seed: u64) -> SoRun {
so_run_cma!(
ackley_problem(),
ACKLEY_DIM,
-32.768,
32.768,
ACKLEY_BUDGET,
seed
)
}
fn ackley_pso(seed: u64) -> SoRun {
so_run_pso!(
ackley_problem(),
ACKLEY_DIM,
-32.768,
32.768,
ACKLEY_BUDGET,
seed
)
}
fn ackley_tlbo(seed: u64) -> SoRun {
so_run_tlbo!(
ackley_problem(),
ACKLEY_DIM,
-32.768,
32.768,
ACKLEY_BUDGET,
seed
)
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// ZDT3 runners (curated MO subset) // ZDT3 runners (curated MO subset)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
fn zdt3_problem() -> Zdt3 { Zdt3 { dim: ZDT3_DIM } } fn zdt3_problem() -> Zdt3 {
Zdt3 { dim: ZDT3_DIM }
}
fn zdt3_nsga2(seed: u64) -> MoRun { fn zdt3_nsga2(seed: u64) -> MoRun {
let problem = zdt3_problem(); let problem = zdt3_problem();
@@ -1255,11 +1445,18 @@ fn zdt3_nsga2(seed: u64) -> MoRun {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64),
}; };
let pop = 100; let pop = 100;
let config = Nsga2Config { population_size: pop, generations: ZDT3_BUDGET / pop, seed }; let config = Nsga2Config {
population_size: pop,
generations: ZDT3_BUDGET / pop,
seed,
};
let mut opt = Nsga2::new(config, initializer, variation); let mut opt = Nsga2::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt3_moead(seed: u64) -> MoRun { fn zdt3_moead(seed: u64) -> MoRun {
@@ -1280,7 +1477,10 @@ fn zdt3_moead(seed: u64) -> MoRun {
let mut opt = Moead::new(config, initializer, variation); let mut opt = Moead::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt3_ibea(seed: u64) -> MoRun { fn zdt3_ibea(seed: u64) -> MoRun {
@@ -1292,11 +1492,19 @@ fn zdt3_ibea(seed: u64) -> MoRun {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64),
}; };
let pop = 100; let pop = 100;
let config = IbeaConfig { population_size: pop, generations: ZDT3_BUDGET / pop, kappa: 0.05, seed }; let config = IbeaConfig {
population_size: pop,
generations: ZDT3_BUDGET / pop,
kappa: 0.05,
seed,
};
let mut opt = Ibea::new(config, initializer, variation); let mut opt = Ibea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn zdt3_age_moea(seed: u64) -> MoRun { fn zdt3_age_moea(seed: u64) -> MoRun {
@@ -1308,11 +1516,18 @@ fn zdt3_age_moea(seed: u64) -> MoRun {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / ZDT3_DIM as f64),
}; };
let pop = 100; let pop = 100;
let config = AgeMoeaConfig { population_size: pop, generations: ZDT3_BUDGET / pop, seed }; let config = AgeMoeaConfig {
population_size: pop,
generations: ZDT3_BUDGET / pop,
seed,
};
let mut opt = AgeMoea::new(config, initializer, variation); let mut opt = AgeMoea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -1320,7 +1535,10 @@ fn zdt3_age_moea(seed: u64) -> MoRun {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
fn dtlz1_problem() -> Dtlz1 { fn dtlz1_problem() -> Dtlz1 {
Dtlz1 { num_objectives: DTLZ1_OBJECTIVES, dim: DTLZ1_DIM } Dtlz1 {
num_objectives: DTLZ1_OBJECTIVES,
dim: DTLZ1_DIM,
}
} }
fn dtlz1_nsga3(seed: u64) -> MoRun { fn dtlz1_nsga3(seed: u64) -> MoRun {
@@ -1341,7 +1559,10 @@ fn dtlz1_nsga3(seed: u64) -> MoRun {
let mut opt = Nsga3::new(config, initializer, variation); let mut opt = Nsga3::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz1_moead(seed: u64) -> MoRun { fn dtlz1_moead(seed: u64) -> MoRun {
@@ -1362,7 +1583,10 @@ fn dtlz1_moead(seed: u64) -> MoRun {
let mut opt = Moead::new(config, initializer, variation); let mut opt = Moead::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz1_age_moea(seed: u64) -> MoRun { fn dtlz1_age_moea(seed: u64) -> MoRun {
@@ -1374,11 +1598,18 @@ fn dtlz1_age_moea(seed: u64) -> MoRun {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ1_DIM as f64), mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / DTLZ1_DIM as f64),
}; };
let pop = 92; let pop = 92;
let config = AgeMoeaConfig { population_size: pop, generations: DTLZ1_BUDGET / pop, seed }; let config = AgeMoeaConfig {
population_size: pop,
generations: DTLZ1_BUDGET / pop,
seed,
};
let mut opt = AgeMoea::new(config, initializer, variation); let mut opt = AgeMoea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
fn dtlz1_grea(seed: u64) -> MoRun { fn dtlz1_grea(seed: u64) -> MoRun {
@@ -1399,7 +1630,10 @@ fn dtlz1_grea(seed: u64) -> MoRun {
let mut opt = Grea::new(config, initializer, variation); let mut opt = Grea::new(config, initializer, variation);
let t0 = Instant::now(); let t0 = Instant::now();
let result = opt.run(&problem); let result = opt.run(&problem);
MoRun { front: result.pareto_front, wall_ms: t0.elapsed().as_millis() } MoRun {
front: result.pareto_front,
wall_ms: t0.elapsed().as_millis(),
}
} }
/// Mean L2 distance from each front point to the analytical DTLZ1 front /// Mean L2 distance from each front point to the analytical DTLZ1 front
@@ -1424,9 +1658,7 @@ fn mean_distance_to_dtlz1_front(front: &[Candidate<Vec<f64>>]) -> f64 {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
fn run_zdt1_comparison() { fn run_zdt1_comparison() {
println!( println!("== ZDT1 (dim={ZDT1_DIM}, {ZDT1_BUDGET} evals/run × {SEEDS} seeds) ==");
"== ZDT1 (dim={ZDT1_DIM}, {ZDT1_BUDGET} evals/run × {SEEDS} seeds) =="
);
println!("metric arrows: hypervolume↑ (higher better), others↓ (lower better)"); println!("metric arrows: hypervolume↑ (higher better), others↓ (lower better)");
println!(); println!();
println!( println!(
@@ -1461,10 +1693,11 @@ fn run_zdt1_comparison() {
.iter() .iter()
.map(|r| hypervolume_2d(&r.front, &zdt1_objs, ZDT1_REFERENCE)) .map(|r| hypervolume_2d(&r.front, &zdt1_objs, ZDT1_REFERENCE))
.collect(); .collect();
let sp: Vec<f64> = let sp: Vec<f64> = runs.iter().map(|r| spacing(&r.front, &zdt1_objs)).collect();
runs.iter().map(|r| spacing(&r.front, &zdt1_objs)).collect(); let l2: Vec<f64> = runs
let l2: Vec<f64> = .iter()
runs.iter().map(|r| mean_l2_to_zdt1_front(&r.front)).collect(); .map(|r| mean_l2_to_zdt1_front(&r.front))
.collect();
let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect(); let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect();
let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect(); let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect();
@@ -1488,9 +1721,7 @@ fn run_zdt1_comparison() {
fn run_dtlz2_comparison() { fn run_dtlz2_comparison() {
println!(); println!();
println!( println!("== DTLZ2 (3-obj, dim={DTLZ2_DIM}, {DTLZ2_BUDGET} evals/run × {SEEDS} seeds) ==");
"== DTLZ2 (3-obj, dim={DTLZ2_DIM}, {DTLZ2_BUDGET} evals/run × {SEEDS} seeds) =="
);
println!("Pareto front: unit sphere octant (Σf²=1, all f≥0); 'mean dist' is |‖f‖−1|"); println!("Pareto front: unit sphere octant (Σf²=1, all f≥0); 'mean dist' is |‖f‖−1|");
println!(); println!();
println!( println!(
@@ -1520,10 +1751,14 @@ fn run_dtlz2_comparison() {
for (name, runner) in runners { for (name, runner) in runners {
let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect(); let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect();
let dist: Vec<f64> = let dist: Vec<f64> = runs
runs.iter().map(|r| mean_distance_to_dtlz2_front(&r.front)).collect(); .iter()
let sp: Vec<f64> = .map(|r| mean_distance_to_dtlz2_front(&r.front))
runs.iter().map(|r| spacing(&r.front, &dtlz2_objs)).collect(); .collect();
let sp: Vec<f64> = runs
.iter()
.map(|r| spacing(&r.front, &dtlz2_objs))
.collect();
let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect(); let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect();
let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect(); let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect();
@@ -1545,9 +1780,7 @@ fn run_dtlz2_comparison() {
fn run_rastrigin_comparison() { fn run_rastrigin_comparison() {
println!(); println!();
println!( println!("== Rastrigin (dim={RASTRIGIN_DIM}, {RASTRIGIN_BUDGET} evals/run × {SEEDS} seeds) ==");
"== Rastrigin (dim={RASTRIGIN_DIM}, {RASTRIGIN_BUDGET} evals/run × {SEEDS} seeds) =="
);
println!("global minimum: f = 0 (lower is better)"); println!("global minimum: f = 0 (lower is better)");
println!(); println!();
println!("{:<14} {:>20} {:>10}", "algorithm", "best f", "ms"); println!("{:<14} {:>20} {:>10}", "algorithm", "best f", "ms");
@@ -1668,7 +1901,10 @@ fn run_zdt3_comparison() {
]; ];
for (name, runner) in runners { for (name, runner) in runners {
let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect(); let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect();
let hv: Vec<f64> = runs.iter().map(|r| hypervolume_2d(&r.front, &objs, ZDT3_REFERENCE)).collect(); let hv: Vec<f64> = runs
.iter()
.map(|r| hypervolume_2d(&r.front, &objs, ZDT3_REFERENCE))
.collect();
let sp: Vec<f64> = runs.iter().map(|r| spacing(&r.front, &objs)).collect(); let sp: Vec<f64> = runs.iter().map(|r| spacing(&r.front, &objs)).collect();
let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect(); let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect();
let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect(); let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect();
@@ -1708,7 +1944,10 @@ fn run_dtlz1_comparison() {
]; ];
for (name, runner) in runners { for (name, runner) in runners {
let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect(); let runs: Vec<MoRun> = (0..SEEDS).map(runner).collect();
let dist: Vec<f64> = runs.iter().map(|r| mean_distance_to_dtlz1_front(&r.front)).collect(); let dist: Vec<f64> = runs
.iter()
.map(|r| mean_distance_to_dtlz1_front(&r.front))
.collect();
let sp: Vec<f64> = runs.iter().map(|r| spacing(&r.front, &objs)).collect(); let sp: Vec<f64> = runs.iter().map(|r| spacing(&r.front, &objs)).collect();
let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect(); let fs: Vec<f64> = runs.iter().map(|r| r.front.len() as f64).collect();
let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect(); let ms: Vec<f64> = runs.iter().map(|r| r.wall_ms as f64).collect();
+4 -1
View File
@@ -26,7 +26,10 @@ where
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!(objectives.is_single_objective(), "HillClimber needs one objective"); assert!(
objectives.is_single_objective(),
"HillClimber needs one objective"
);
let mut rng = rng_from_seed(self.seed); let mut rng = rng_from_seed(self.seed);
let mut variation = GaussianMutation { sigma: self.sigma }; let mut variation = GaussianMutation { sigma: self.sigma };
+55 -16
View File
@@ -152,7 +152,10 @@ impl JigglyTuning {
let mut rng = StdRng::seed_from_u64(day_seed); let mut rng = StdRng::seed_from_u64(day_seed);
let mut expire = s + rt; let mut expire = s + rt;
// Boot press at workday start: user presses to begin cycle 1. // Boot press at workday start: user presses to begin cycle 1.
let mut o = DayOutcome { presses: 1, ..Default::default() }; let mut o = DayOutcome {
presses: 1,
..Default::default()
};
// Allow the loop to extend past the larger of (workday end, last // Allow the loop to extend past the larger of (workday end, last
// possible cycle end given any in-loop expire bumps). Cap at one // possible cycle end given any in-loop expire bumps). Cap at one
// extra cycle's worth so a long string of presses can't blow the // extra cycle's worth so a long string of presses can't blow the
@@ -348,7 +351,11 @@ fn print_header() {
} }
fn print_row(label: &str, r: &Row) { fn print_row(label: &str, r: &Row) {
let prefix = if label.is_empty() { String::new() } else { format!("{label} ") }; let prefix = if label.is_empty() {
String::new()
} else {
format!("{label} ")
};
println!( println!(
"{}{:<6} {:>3} {:>3} {:>3} {:>9} {:>9} {:>7.2}/d {:>8} {:>6.1}%", "{}{:<6} {:>3} {:>3} {:>3} {:>9} {:>9} {:>7.2}/d {:>8} {:>6.1}%",
prefix, prefix,
@@ -440,7 +447,11 @@ fn main() {
println!("=== Pareto front (sorted by lunch sleep, descending) ==="); println!("=== Pareto front (sorted by lunch sleep, descending) ===");
print_header(); print_header();
rows.sort_by(|a, b| b.lunch.partial_cmp(&a.lunch).unwrap_or(std::cmp::Ordering::Equal)); rows.sort_by(|a, b| {
b.lunch
.partial_cmp(&a.lunch)
.unwrap_or(std::cmp::Ordering::Equal)
});
for r in rows.iter().take(15) { for r in rows.iter().take(15) {
print_row("", r); print_row("", r);
} }
@@ -507,8 +518,12 @@ fn main() {
let shipping_candidate_idx = candidates.len(); let shipping_candidate_idx = candidates.len();
candidates.push(("shipping default".to_string(), shipping_row.clone())); candidates.push(("shipping default".to_string(), shipping_row.clone()));
let scores = let scores = compute_weighted_scores(
compute_weighted_scores(&candidates.iter().map(|(_, r)| r.clone()).collect::<Vec<_>>()); &candidates
.iter()
.map(|(_, r)| r.clone())
.collect::<Vec<_>>(),
);
let mut ranked: Vec<(usize, f64)> = scores.iter().copied().enumerate().collect(); let mut ranked: Vec<(usize, f64)> = scores.iter().copied().enumerate().collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
@@ -529,7 +544,10 @@ fn main() {
" balance bonus: min(yellow_width, red_width), saturates at {:.0} min", " balance bonus: min(yellow_width, red_width), saturates at {:.0} min",
BALANCE_SATURATION_MIN, BALANCE_SATURATION_MIN,
); );
println!(" candidate set: {} Pareto-front rows + 1 shipping default", rows.len()); println!(
" candidate set: {} Pareto-front rows + 1 shipping default",
rows.len()
);
println!(); println!();
println!("{:>4} {:>5} source", "rank", "score"); println!("{:>4} {:>5} source", "rank", "score");
print_header(); print_header();
@@ -548,7 +566,10 @@ fn main() {
.map(|p| p + 1) .map(|p| p + 1)
.unwrap_or(0); .unwrap_or(0);
let max_work = candidates.iter().map(|(_, r)| r.work_fail).fold(0.0, f64::max); let max_work = candidates
.iter()
.map(|(_, r)| r.work_fail)
.fold(0.0, f64::max);
println!("=== RECOMMENDED PICK ({top_label}) ==="); println!("=== RECOMMENDED PICK ({top_label}) ===");
println!( println!(
@@ -591,9 +612,7 @@ fn main() {
"{:.2} button presses/day total — {}", "{:.2} button presses/day total — {}",
top.presses, press_note, top.presses, press_note,
); );
println!( println!(" (counts: boot + 13:00 retap + warning-phase reactions + death-restarts)");
" (counts: boot + 13:00 retap + warning-phase reactions + death-restarts)"
);
println!( println!(
" • warning phases: yellow {} min, red {} min, fast-red {} min (balance score {:.2})", " • warning phases: yellow {} min, red {} min, fast-red {} min (balance score {:.2})",
yellow_w, yellow_w,
@@ -629,12 +648,24 @@ fn main() {
/// phases, computed as `min(YA - RA, RA - FRA)` saturated at /// phases, computed as `min(YA - RA, RA - FRA)` saturated at
/// `BALANCE_SATURATION_MIN`. /// `BALANCE_SATURATION_MIN`.
fn compute_weighted_scores(rows: &[Row]) -> Vec<f64> { fn compute_weighted_scores(rows: &[Row]) -> Vec<f64> {
let work_min = rows.iter().map(|r| r.work_fail).fold(f64::INFINITY, f64::min); let work_min = rows
let work_max = rows.iter().map(|r| r.work_fail).fold(f64::NEG_INFINITY, f64::max); .iter()
.map(|r| r.work_fail)
.fold(f64::INFINITY, f64::min);
let work_max = rows
.iter()
.map(|r| r.work_fail)
.fold(f64::NEG_INFINITY, f64::max);
let lunch_min = rows.iter().map(|r| r.lunch).fold(f64::INFINITY, f64::min); let lunch_min = rows.iter().map(|r| r.lunch).fold(f64::INFINITY, f64::min);
let lunch_max = rows.iter().map(|r| r.lunch).fold(f64::NEG_INFINITY, f64::max); let lunch_max = rows
.iter()
.map(|r| r.lunch)
.fold(f64::NEG_INFINITY, f64::max);
let after_min = rows.iter().map(|r| r.after).fold(f64::INFINITY, f64::min); let after_min = rows.iter().map(|r| r.after).fold(f64::INFINITY, f64::min);
let after_max = rows.iter().map(|r| r.after).fold(f64::NEG_INFINITY, f64::max); let after_max = rows
.iter()
.map(|r| r.after)
.fold(f64::NEG_INFINITY, f64::max);
rows.iter() rows.iter()
.map(|r| { .map(|r| {
@@ -676,12 +707,20 @@ fn balance_score_for(r: &Row) -> f64 {
/// Normalize a minimize-direction value to `[0, 1]` (best→1, worst→0). /// Normalize a minimize-direction value to `[0, 1]` (best→1, worst→0).
fn norm_min(v: f64, lo: f64, hi: f64) -> f64 { fn norm_min(v: f64, lo: f64, hi: f64) -> f64 {
if (hi - lo).abs() < 1e-12 { 1.0 } else { (hi - v) / (hi - lo) } if (hi - lo).abs() < 1e-12 {
1.0
} else {
(hi - v) / (hi - lo)
}
} }
/// Normalize a maximize-direction value to `[0, 1]` (best→1, worst→0). /// Normalize a maximize-direction value to `[0, 1]` (best→1, worst→0).
fn norm_max(v: f64, lo: f64, hi: f64) -> f64 { fn norm_max(v: f64, lo: f64, hi: f64) -> f64 {
if (hi - lo).abs() < 1e-12 { 1.0 } else { (v - lo) / (hi - lo) } if (hi - lo).abs() < 1e-12 {
1.0
} else {
(v - lo) / (hi - lo)
}
} }
/// Render `worst / best` as e.g. "7.5×" for the recommendation rationale. /// Render `worst / best` as e.g. "7.5×" for the recommendation rationale.
+5 -1
View File
@@ -24,7 +24,11 @@ impl Problem for Sphere2D {
fn main() { fn main() {
let initializer = RealBounds::new(vec![(-5.0, 5.0), (-5.0, 5.0)]); let initializer = RealBounds::new(vec![(-5.0, 5.0), (-5.0, 5.0)]);
let config = RandomSearchConfig { iterations: 500, batch_size: 1, seed: 7 }; let config = RandomSearchConfig {
iterations: 500,
batch_size: 1,
seed: 7,
};
let mut optimizer = RandomSearch::new(config, initializer); let mut optimizer = RandomSearch::new(config, initializer);
let result = optimizer.run(&Sphere2D); let result = optimizer.run(&Sphere2D);
+5 -1
View File
@@ -26,7 +26,11 @@ impl Problem for SchafferN1 {
fn main() { fn main() {
let initializer = RealBounds::new(vec![(-5.0, 5.0)]); let initializer = RealBounds::new(vec![(-5.0, 5.0)]);
let variation = GaussianMutation { sigma: 0.2 }; let variation = GaussianMutation { sigma: 0.2 };
let config = Nsga2Config { population_size: 60, generations: 80, seed: 42 }; let config = Nsga2Config {
population_size: 60,
generations: 80,
seed: 42,
};
let mut optimizer = Nsga2::new(config, initializer, variation); let mut optimizer = Nsga2::new(config, initializer, variation);
let result = optimizer.run(&SchafferN1); let result = optimizer.run(&SchafferN1);
+53 -20
View File
@@ -26,7 +26,11 @@ pub struct AgeMoeaConfig {
impl Default for AgeMoeaConfig { impl Default for AgeMoeaConfig {
fn default() -> Self { fn default() -> Self {
Self { population_size: 100, generations: 250, seed: 42 } Self {
population_size: 100,
generations: 250,
seed: 42,
}
} }
} }
@@ -49,7 +53,11 @@ pub struct AgeMoea<I, V> {
impl<I, V> AgeMoea<I, V> { impl<I, V> AgeMoea<I, V> {
/// Construct an `AgeMoea`. /// Construct an `AgeMoea`.
pub fn new(config: AgeMoeaConfig, initializer: I, variation: V) -> Self { pub fn new(config: AgeMoeaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -61,7 +69,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "AgeMoea population_size must be > 0"); assert!(
self.config.population_size > 0,
"AgeMoea population_size must be > 0"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
@@ -77,10 +88,15 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "AgeMoea variation returned no children"); assert!(
!children.is_empty(),
"AgeMoea variation returned no children"
);
for child in children { for child in children {
if offspring_decisions.len() >= n { if offspring_decisions.len() >= n {
break; break;
@@ -153,7 +169,11 @@ fn environmental_selection<D: Clone>(
.iter() .iter()
.map(|c| { .map(|c| {
let oriented = objectives.as_minimization(&c.evaluation.objectives); let oriented = objectives.as_minimization(&c.evaluation.objectives);
oriented.iter().enumerate().map(|(k, v)| (v - ideal[k]).max(0.0)).collect() oriented
.iter()
.enumerate()
.map(|(k, v)| (v - ideal[k]).max(0.0))
.collect()
}) })
.collect(); .collect();
@@ -199,15 +219,14 @@ fn lp_norm(v: &[f64], p: f64) -> f64 {
} }
fn lp_distance(a: &[f64], b: &[f64], p: f64) -> f64 { fn lp_distance(a: &[f64], b: &[f64], p: f64) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs().powf(p)).sum::<f64>().powf(1.0 / p) a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).abs().powf(p))
.sum::<f64>()
.powf(1.0 / p)
} }
fn nearest_neighbor_distance( fn nearest_neighbor_distance(i: usize, translated: &[Vec<f64>], selected: &[usize], p: f64) -> f64 {
i: usize,
translated: &[Vec<f64>],
selected: &[usize],
p: f64,
) -> f64 {
if selected.is_empty() { if selected.is_empty() {
return f64::INFINITY; return f64::INFINITY;
} }
@@ -295,7 +314,11 @@ mod tests {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
AgeMoea::new( AgeMoea::new(
AgeMoeaConfig { population_size: 20, generations: 15, seed }, AgeMoeaConfig {
population_size: 20,
generations: 15,
seed,
},
initializer, initializer,
variation, variation,
) )
@@ -315,10 +338,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
@@ -332,7 +361,11 @@ mod tests {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
let mut opt = AgeMoea::new( let mut opt = AgeMoea::new(
AgeMoeaConfig { population_size: 0, generations: 1, seed: 0 }, AgeMoeaConfig {
population_size: 0,
generations: 1,
seed: 0,
},
initializer, initializer,
variation, variation,
); );
+21 -10
View File
@@ -70,10 +70,16 @@ impl AntColonyTsp {
/// and has a zero diagonal. /// and has a zero diagonal.
pub fn new(config: AntColonyTspConfig, distances: Vec<Vec<f64>>) -> Self { pub fn new(config: AntColonyTspConfig, distances: Vec<Vec<f64>>) -> Self {
let n = distances.len(); let n = distances.len();
assert!(n >= 2, "AntColonyTsp distances matrix must have >= 2 cities"); assert!(
n >= 2,
"AntColonyTsp distances matrix must have >= 2 cities"
);
for (i, row) in distances.iter().enumerate() { for (i, row) in distances.iter().enumerate() {
assert_eq!(row.len(), n, "AntColonyTsp distances matrix must be square"); assert_eq!(row.len(), n, "AntColonyTsp distances matrix must be square");
assert_eq!(row[i], 0.0, "AntColonyTsp distance from city to itself must be 0"); assert_eq!(
row[i], 0.0,
"AntColonyTsp distance from city to itself must be 0"
);
} }
Self { config, distances } Self { config, distances }
} }
@@ -99,12 +105,15 @@ where
let eta: Vec<Vec<f64>> = self let eta: Vec<Vec<f64>> = self
.distances .distances
.iter() .iter()
.map(|row| row.iter().map(|&d| if d > 0.0 { 1.0 / d } else { 0.0 }).collect()) .map(|row| {
row.iter()
.map(|&d| if d > 0.0 { 1.0 / d } else { 0.0 })
.collect()
})
.collect(); .collect();
// Pheromone matrix. // Pheromone matrix.
let mut pheromone: Vec<Vec<f64>> = let mut pheromone: Vec<Vec<f64>> = vec![vec![self.config.initial_pheromone; n]; n];
vec![vec![self.config.initial_pheromone; n]; n];
let mut best_decision: Option<Vec<usize>> = None; let mut best_decision: Option<Vec<usize>> = None;
let mut best_eval: Option<crate::core::evaluation::Evaluation> = None; let mut best_eval: Option<crate::core::evaluation::Evaluation> = None;
@@ -153,7 +162,12 @@ where
// Pheromone deposit on each ant's tour. // Pheromone deposit on each ant's tour.
for (tour, eval) in tours.iter().zip(tour_evals.iter()) { for (tour, eval) in tours.iter().zip(tour_evals.iter()) {
let length = eval.objectives.first().copied().unwrap_or(f64::INFINITY).max(1e-12); let length = eval
.objectives
.first()
.copied()
.unwrap_or(f64::INFINITY)
.max(1e-12);
let deposit = self.config.deposit / length; let deposit = self.config.deposit / length;
for w in tour.windows(2) { for w in tour.windows(2) {
let (i, j) = (w[0], w[1]); let (i, j) = (w[0], w[1]);
@@ -313,10 +327,7 @@ mod tests {
type Decision = Vec<usize>; type Decision = Vec<usize>;
fn objectives(&self) -> ObjectiveSpace { fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("a"), Objective::minimize("b")])
Objective::minimize("a"),
Objective::minimize("b"),
])
} }
fn evaluate(&self, _tour: &Vec<usize>) -> Evaluation { fn evaluate(&self, _tour: &Vec<usize>) -> Evaluation {
+47 -25
View File
@@ -85,8 +85,14 @@ where
self.config.initial_samples >= 2, self.config.initial_samples >= 2,
"BayesianOpt initial_samples must be >= 2", "BayesianOpt initial_samples must be >= 2",
); );
assert!(self.config.signal_variance > 0.0, "BayesianOpt signal_variance must be > 0"); assert!(
assert!(self.config.noise_variance > 0.0, "BayesianOpt noise_variance must be > 0"); self.config.signal_variance > 0.0,
"BayesianOpt signal_variance must be > 0"
);
assert!(
self.config.noise_variance > 0.0,
"BayesianOpt noise_variance must be > 0"
);
assert!( assert!(
self.config.acquisition_samples >= 1, self.config.acquisition_samples >= 1,
"BayesianOpt acquisition_samples must be >= 1", "BayesianOpt acquisition_samples must be >= 1",
@@ -99,25 +105,24 @@ where
let direction = objectives.objectives[0].direction; let direction = objectives.objectives[0].direction;
let dim = self.bounds.bounds.len(); let dim = self.bounds.bounds.len();
if let Some(ls) = &self.config.length_scales { if let Some(ls) = &self.config.length_scales {
assert_eq!(ls.len(), dim, "BayesianOpt length_scales.len() must equal dim"); assert_eq!(
ls.len(),
dim,
"BayesianOpt length_scales.len() must equal dim"
);
} }
let length_scales: Vec<f64> = self let length_scales: Vec<f64> = self.config.length_scales.clone().unwrap_or_else(|| {
.config self.bounds
.length_scales .bounds
.clone() .iter()
.unwrap_or_else(|| { .map(|&(lo, hi)| 0.2 * (hi - lo).max(1e-9))
self.bounds .collect()
.bounds });
.iter()
.map(|&(lo, hi)| 0.2 * (hi - lo).max(1e-9))
.collect()
});
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
// ---------------- Initial random design ---------------- // ---------------- Initial random design ----------------
let mut decisions: Vec<Vec<f64>> = Vec::with_capacity( let mut decisions: Vec<Vec<f64>> =
self.config.initial_samples + self.config.iterations, Vec::with_capacity(self.config.initial_samples + self.config.iterations);
);
let mut targets: Vec<f64> = Vec::with_capacity(decisions.capacity()); let mut targets: Vec<f64> = Vec::with_capacity(decisions.capacity());
let mut evaluations = Vec::with_capacity(decisions.capacity()); let mut evaluations = Vec::with_capacity(decisions.capacity());
for _ in 0..self.config.initial_samples { for _ in 0..self.config.initial_samples {
@@ -153,8 +158,7 @@ where
} }
}; };
let best_target = let best_target = targets.iter().cloned().fold(f64::INFINITY, f64::min);
targets.iter().cloned().fold(f64::INFINITY, f64::min);
// Maximize EI by best-of-N random sampling. // Maximize EI by best-of-N random sampling.
let mut best_x = sample_uniform_in_bounds(&self.bounds, &mut rng); let mut best_x = sample_uniform_in_bounds(&self.bounds, &mut rng);
@@ -183,7 +187,11 @@ where
.collect(); .collect();
let mut best_idx = 0; let mut best_idx = 0;
for i in 1..final_pop.len() { for i in 1..final_pop.len() {
if better(&final_pop[i].evaluation, &final_pop[best_idx].evaluation, direction) { if better(
&final_pop[i].evaluation,
&final_pop[best_idx].evaluation,
direction,
) {
best_idx = i; best_idx = i;
} }
} }
@@ -232,7 +240,13 @@ fn sample_uniform_in_bounds(bounds: &RealBounds, rng: &mut Rng) -> Vec<f64> {
bounds bounds
.bounds .bounds
.iter() .iter()
.map(|&(lo, hi)| if lo == hi { lo } else { lo + (hi - lo) * rng.random::<f64>() }) .map(|&(lo, hi)| {
if lo == hi {
lo
} else {
lo + (hi - lo) * rng.random::<f64>()
}
})
.collect() .collect()
} }
@@ -289,10 +303,19 @@ impl GpPosterior {
let n = self.decisions.len(); let n = self.decisions.len();
let mut k_star = vec![0.0_f64; n]; let mut k_star = vec![0.0_f64; n];
for (i, k_star_i) in k_star.iter_mut().enumerate() { for (i, k_star_i) in k_star.iter_mut().enumerate() {
*k_star_i = rbf_kernel(x, &self.decisions[i], &self.length_scales, self.signal_variance); *k_star_i = rbf_kernel(
x,
&self.decisions[i],
&self.length_scales,
self.signal_variance,
);
} }
let _ = n; let _ = n;
let mu: f64 = k_star.iter().zip(self.alpha.iter()).map(|(a, b)| a * b).sum(); let mu: f64 = k_star
.iter()
.zip(self.alpha.iter())
.map(|(a, b)| a * b)
.sum();
// Var = k(x,x) - k_star^T · K^{-1} · k_star // Var = k(x,x) - k_star^T · K^{-1} · k_star
// Compute K^{-1}·k_star = solve_upper_transpose(L, solve_lower(L, k_star)) // Compute K^{-1}·k_star = solve_upper_transpose(L, solve_lower(L, k_star))
let v_temp = crate::internal::cholesky::solve_lower(&self.chol_l, &k_star); let v_temp = crate::internal::cholesky::solve_lower(&self.chol_l, &k_star);
@@ -335,8 +358,7 @@ fn erf(x: f64) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 }; let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs(); let x = x.abs();
let t = 1.0 / (1.0 + p * x); let t = 1.0 / (1.0 + p * x);
let y = 1.0 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
- (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y sign * y
} }
+17 -17
View File
@@ -122,9 +122,7 @@ where
// Standard CMA-ES strategy parameters (Hansen tutorial §7.1). // Standard CMA-ES strategy parameters (Hansen tutorial §7.1).
// --------------------------------------------------------------- // ---------------------------------------------------------------
let c_sigma = (mu_eff + 2.0) / (n_f + mu_eff + 5.0); let c_sigma = (mu_eff + 2.0) / (n_f + mu_eff + 5.0);
let d_sigma = 1.0 let d_sigma = 1.0 + 2.0 * ((mu_eff - 1.0) / (n_f + 1.0)).sqrt().max(0.0) + c_sigma;
+ 2.0 * ((mu_eff - 1.0) / (n_f + 1.0)).sqrt().max(0.0)
+ c_sigma;
let c_c = (4.0 + mu_eff / n_f) / (n_f + 4.0 + 2.0 * mu_eff / n_f); let c_c = (4.0 + mu_eff / n_f) / (n_f + 4.0 + 2.0 * mu_eff / n_f);
let c_1 = 2.0 / ((n_f + 1.3).powi(2) + mu_eff); let c_1 = 2.0 / ((n_f + 1.3).powi(2) + mu_eff);
let c_mu = ((1.0 - c_1) * 2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) let c_mu = ((1.0 - c_1) * 2.0 * (mu_eff - 2.0 + 1.0 / mu_eff)
@@ -150,7 +148,11 @@ where
.map(|(v, &(lo, hi))| v.clamp(lo, hi)) .map(|(v, &(lo, hi))| v.clamp(lo, hi))
.collect() .collect()
} else { } else {
self.bounds.bounds.iter().map(|&(lo, hi)| 0.5 * (lo + hi)).collect() self.bounds
.bounds
.iter()
.map(|&(lo, hi)| 0.5 * (lo + hi))
.collect()
}; };
let mut sigma = self.config.initial_sigma; let mut sigma = self.config.initial_sigma;
// Covariance C, eigenvectors B, eigenvalues d (square roots of eigenvalues of C). // Covariance C, eigenvectors B, eigenvalues d (square roots of eigenvalues of C).
@@ -181,10 +183,7 @@ where
let (eigenvalues, eigenvectors) = symmetric_eigen(&c_matrix, 1e-14, 100); let (eigenvalues, eigenvectors) = symmetric_eigen(&c_matrix, 1e-14, 100);
// eigenvectors is sorted descending; we don't depend on order // eigenvectors is sorted descending; we don't depend on order
// for sampling correctness, but we do need positive eigenvalues. // for sampling correctness, but we do need positive eigenvalues.
d = eigenvalues d = eigenvalues.iter().map(|&v| v.max(1e-20).sqrt()).collect();
.iter()
.map(|&v| v.max(1e-20).sqrt())
.collect();
// B is the matrix whose columns are the eigenvectors. The // B is the matrix whose columns are the eigenvectors. The
// helper returns `eigenvectors[i]` as the i-th *eigenvector*, // helper returns `eigenvectors[i]` as the i-th *eigenvector*,
// so b[r][c] should equal eigenvectors[c][r]. // so b[r][c] should equal eigenvectors[c][r].
@@ -232,7 +231,11 @@ where
// Sort offspring by fitness ascending (best first). // Sort offspring by fitness ascending (best first).
let mut order: Vec<usize> = (0..lambda).collect(); let mut order: Vec<usize> = (0..lambda).collect();
order.sort_by(|&a, &b_| { order.sort_by(|&a, &b_| {
compare_so(&evaluated[a].evaluation, &evaluated[b_].evaluation, direction) compare_so(
&evaluated[a].evaluation,
&evaluated[b_].evaluation,
direction,
)
}); });
// ----- Recompute mean from the μ best (weighted average of x) ----- // ----- Recompute mean from the μ best (weighted average of x) -----
@@ -284,13 +287,13 @@ where
// ----- Evolution path for C: p_c = (1 - c_c) p_c + h_σ · sqrt(c_c (2 - c_c) μ_eff) · (m_new - m_old)/σ ----- // ----- Evolution path for C: p_c = (1 - c_c) p_c + h_σ · sqrt(c_c (2 - c_c) μ_eff) · (m_new - m_old)/σ -----
let factor_p_c = h_sigma * (c_c * (2.0 - c_c) * mu_eff).sqrt(); let factor_p_c = h_sigma * (c_c * (2.0 - c_c) * mu_eff).sqrt();
for i in 0..n { for i in 0..n {
p_c[i] = (1.0 - c_c) * p_c[i] p_c[i] = (1.0 - c_c) * p_c[i] + factor_p_c * (mean[i] - old_mean[i]) / sigma;
+ factor_p_c * (mean[i] - old_mean[i]) / sigma;
} }
// ----- Covariance matrix update (rank-1 + rank-μ) ----- // ----- Covariance matrix update (rank-1 + rank-μ) -----
let delta_h = (1.0 - h_sigma) * c_c * (2.0 - c_c); let delta_h = (1.0 - h_sigma) * c_c * (2.0 - c_c);
#[allow(clippy::needless_range_loop)] // body uses both i and j to index c_matrix and offspring. #[allow(clippy::needless_range_loop)]
// body uses both i and j to index c_matrix and offspring.
for i in 0..n { for i in 0..n {
for j in 0..n { for j in 0..n {
let mut update = (1.0 - c_1 - c_mu) * c_matrix[i][j] let mut update = (1.0 - c_1 - c_mu) * c_matrix[i][j]
@@ -443,7 +446,7 @@ mod tests {
generations: 30, generations: 30,
initial_sigma: 0.5, initial_sigma: 0.5,
eigen_decomposition_period: 1, eigen_decomposition_period: 1,
initial_mean: None, initial_mean: None,
seed: 99, seed: 99,
}; };
let mut a = CmaEs::new(cfg.clone(), RealBounds::new(vec![(-5.0, 5.0)])); let mut a = CmaEs::new(cfg.clone(), RealBounds::new(vec![(-5.0, 5.0)]));
@@ -459,10 +462,7 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "single-objective")] #[should_panic(expected = "single-objective")]
fn multi_objective_panics() { fn multi_objective_panics() {
let mut opt = CmaEs::new( let mut opt = CmaEs::new(CmaEsConfig::default(), RealBounds::new(vec![(-5.0, 5.0)]));
CmaEsConfig::default(),
RealBounds::new(vec![(-5.0, 5.0)]),
);
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
+14 -6
View File
@@ -91,8 +91,10 @@ where
}; };
let initial_pop = evaluate_batch(problem, decisions.clone()); let initial_pop = evaluate_batch(problem, decisions.clone());
let mut evaluations = initial_pop.len(); let mut evaluations = initial_pop.len();
let mut evals: Vec<f64> = let mut evals: Vec<f64> = initial_pop
initial_pop.iter().map(|c| c.evaluation.objectives[0]).collect(); .iter()
.map(|c| c.evaluation.objectives[0])
.collect();
for _gen in 0..self.config.generations { for _gen in 0..self.config.generations {
// Phase 1 (serial): construct one trial per target. RNG state is // Phase 1 (serial): construct one trial per target. RNG state is
@@ -151,7 +153,11 @@ where
} }
} }
fn pick_three_distinct(n: usize, exclude: usize, rng: &mut crate::core::rng::Rng) -> (usize, usize, usize) { fn pick_three_distinct(
n: usize,
exclude: usize,
rng: &mut crate::core::rng::Rng,
) -> (usize, usize, usize) {
let pick = |rng: &mut crate::core::rng::Rng, taken: &[usize]| -> usize { let pick = |rng: &mut crate::core::rng::Rng, taken: &[usize]| -> usize {
loop { loop {
let v = rng.random_range(0..n); let v = rng.random_range(0..n);
@@ -185,7 +191,10 @@ mod tests {
); );
let r = opt.run(&Sphere1D); let r = opt.run(&Sphere1D);
let best = r.best.unwrap(); let best = r.best.unwrap();
assert!(best.evaluation.objectives[0] < 1e-3, "DE should converge near 0"); assert!(
best.evaluation.objectives[0] < 1e-3,
"DE should converge near 0"
);
} }
#[test] #[test]
@@ -197,8 +206,7 @@ mod tests {
crossover_probability: 0.7, crossover_probability: 0.7,
seed: 99, seed: 99,
}; };
let mut a = let mut a = DifferentialEvolution::new(cfg.clone(), RealBounds::new(vec![(-5.0, 5.0)]));
DifferentialEvolution::new(cfg.clone(), RealBounds::new(vec![(-5.0, 5.0)]));
let mut b = DifferentialEvolution::new(cfg, RealBounds::new(vec![(-5.0, 5.0)])); let mut b = DifferentialEvolution::new(cfg, RealBounds::new(vec![(-5.0, 5.0)]));
let ra = a.run(&Sphere1D); let ra = a.run(&Sphere1D);
let rb = b.run(&Sphere1D); let rb = b.run(&Sphere1D);
+25 -13
View File
@@ -52,7 +52,11 @@ pub struct EpsilonMoea<I, V> {
impl<I, V> EpsilonMoea<I, V> { impl<I, V> EpsilonMoea<I, V> {
/// Construct an `EpsilonMoea`. /// Construct an `EpsilonMoea`.
pub fn new(config: EpsilonMoeaConfig, initializer: I, variation: V) -> Self { pub fn new(config: EpsilonMoeaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -64,7 +68,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "EpsilonMoea population_size must be > 0"); assert!(
self.config.population_size > 0,
"EpsilonMoea population_size must be > 0"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
assert_eq!( assert_eq!(
@@ -110,7 +117,10 @@ where
}; };
let parents = vec![parent_a, parent_b]; let parents = vec![parent_a, parent_b];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "EpsilonMoea variation returned no children"); assert!(
!children.is_empty(),
"EpsilonMoea variation returned no children"
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
@@ -205,12 +215,8 @@ fn insert_into_epsilon_archive<D: Clone>(
} }
if let Some(idx) = child_box_index { if let Some(idx) = child_box_index {
// Same box: keep whichever is closer to box's ideal corner. // Same box: keep whichever is closer to box's ideal corner.
let member_corner_dist = corner_distance( let member_corner_dist =
&archive[idx].evaluation, corner_distance(&archive[idx].evaluation, objectives, epsilon, &child_box);
objectives,
epsilon,
&child_box,
);
if child_corner_dist < member_corner_dist { if child_corner_dist < member_corner_dist {
archive[idx] = child; archive[idx] = child;
} }
@@ -302,10 +308,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+15 -11
View File
@@ -59,7 +59,11 @@ pub struct GeneticAlgorithm<I, V> {
impl<I, V> GeneticAlgorithm<I, V> { impl<I, V> GeneticAlgorithm<I, V> {
/// Construct a `GeneticAlgorithm`. /// Construct a `GeneticAlgorithm`.
pub fn new(config: GeneticAlgorithmConfig, initializer: I, variation: V) -> Self { pub fn new(config: GeneticAlgorithmConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -109,7 +113,10 @@ where
&mut rng, &mut rng,
); );
let children = self.variation.vary(&parents_decisions, &mut rng); let children = self.variation.vary(&parents_decisions, &mut rng);
assert!(!children.is_empty(), "GeneticAlgorithm variation returned no children"); assert!(
!children.is_empty(),
"GeneticAlgorithm variation returned no children"
);
for child in children { for child in children {
if offspring_decisions.len() >= n { if offspring_decisions.len() >= n {
break; break;
@@ -123,13 +130,8 @@ where
evaluations += offspring.len(); evaluations += offspring.len();
// --- Phase 3: survival = elites + best offspring --- // --- Phase 3: survival = elites + best offspring ---
population = survival_selection( population =
&population, survival_selection(&population, offspring, direction, n, self.config.elitism);
offspring,
direction,
n,
self.config.elitism,
);
} }
let best = best_candidate(&population, &objectives); let best = best_candidate(&population, &objectives);
@@ -202,8 +204,10 @@ mod tests {
fn make_optimizer( fn make_optimizer(
seed: u64, seed: u64,
) -> GeneticAlgorithm<RealBounds, CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>> ) -> GeneticAlgorithm<
{ RealBounds,
CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>,
> {
let bounds = vec![(-5.0, 5.0)]; let bounds = vec![(-5.0, 5.0)];
let initializer = RealBounds::new(bounds.clone()); let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation { let variation = CompositeVariation {
+29 -10
View File
@@ -51,7 +51,11 @@ pub struct Grea<I, V> {
impl<I, V> Grea<I, V> { impl<I, V> Grea<I, V> {
/// Construct a `Grea`. /// Construct a `Grea`.
pub fn new(config: GreaConfig, initializer: I, variation: V) -> Self { pub fn new(config: GreaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -63,8 +67,14 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "Grea population_size must be > 0"); assert!(
assert!(self.config.grid_divisions >= 1, "Grea grid_divisions must be >= 1"); self.config.population_size > 0,
"Grea population_size must be > 0"
);
assert!(
self.config.grid_divisions >= 1,
"Grea grid_divisions must be >= 1"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
@@ -80,8 +90,10 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "Grea variation returned no children"); assert!(!children.is_empty(), "Grea variation returned no children");
for child in children { for child in children {
@@ -98,7 +110,8 @@ where
let mut combined: Vec<Candidate<P::Decision>> = Vec::with_capacity(2 * n); let mut combined: Vec<Candidate<P::Decision>> = Vec::with_capacity(2 * n);
combined.extend(population); combined.extend(population);
combined.extend(offspring); combined.extend(offspring);
population = environmental_selection(combined, &objectives, n, self.config.grid_divisions); population =
environmental_selection(combined, &objectives, n, self.config.grid_divisions);
} }
let front = pareto_front(&population, &objectives); let front = pareto_front(&population, &objectives);
@@ -253,10 +266,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
} }
+26 -6
View File
@@ -19,7 +19,10 @@ pub struct HillClimberConfig {
impl Default for HillClimberConfig { impl Default for HillClimberConfig {
fn default() -> Self { fn default() -> Self {
Self { iterations: 1000, seed: 42 } Self {
iterations: 1000,
seed: 42,
}
} }
} }
@@ -44,7 +47,11 @@ pub struct HillClimber<I, V> {
impl<I, V> HillClimber<I, V> { impl<I, V> HillClimber<I, V> {
/// Construct a `HillClimber`. /// Construct a `HillClimber`.
pub fn new(config: HillClimberConfig, initializer: I, variation: V) -> Self { pub fn new(config: HillClimberConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -65,7 +72,10 @@ where
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
let mut initial = self.initializer.initialize(1, &mut rng); let mut initial = self.initializer.initialize(1, &mut rng);
assert!(!initial.is_empty(), "HillClimber initializer returned no decisions"); assert!(
!initial.is_empty(),
"HillClimber initializer returned no decisions"
);
let mut current_decision = initial.remove(0); let mut current_decision = initial.remove(0);
let mut current_eval = problem.evaluate(&current_decision); let mut current_eval = problem.evaluate(&current_decision);
let mut evaluations = 1usize; let mut evaluations = 1usize;
@@ -73,7 +83,10 @@ where
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
let parents = vec![current_decision.clone()]; let parents = vec![current_decision.clone()];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "HillClimber variation returned no children"); assert!(
!children.is_empty(),
"HillClimber variation returned no children"
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
@@ -116,7 +129,10 @@ mod tests {
fn make_optimizer(seed: u64) -> HillClimber<RealBounds, GaussianMutation> { fn make_optimizer(seed: u64) -> HillClimber<RealBounds, GaussianMutation> {
HillClimber::new( HillClimber::new(
HillClimberConfig { iterations: 500, seed }, HillClimberConfig {
iterations: 500,
seed,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.3 }, GaussianMutation { sigma: 0.3 },
) )
@@ -127,7 +143,11 @@ mod tests {
let mut opt = make_optimizer(1); let mut opt = make_optimizer(1);
let r = opt.run(&Sphere1D); let r = opt.run(&Sphere1D);
let best = r.best.unwrap(); let best = r.best.unwrap();
assert!(best.evaluation.objectives[0] < 1e-2, "got f = {}", best.evaluation.objectives[0]); assert!(
best.evaluation.objectives[0] < 1e-2,
"got f = {}",
best.evaluation.objectives[0]
);
} }
#[test] #[test]
+41 -13
View File
@@ -61,7 +61,11 @@ pub struct Hype<I, V> {
impl<I, V> Hype<I, V> { impl<I, V> Hype<I, V> {
/// Construct a `Hype`. /// Construct a `Hype`.
pub fn new(config: HypeConfig, initializer: I, variation: V) -> Self { pub fn new(config: HypeConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -73,7 +77,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "Hype population_size must be > 0"); assert!(
self.config.population_size > 0,
"Hype population_size must be > 0"
);
assert!(self.config.mc_samples > 0, "Hype mc_samples must be > 0"); assert!(self.config.mc_samples > 0, "Hype mc_samples must be > 0");
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
@@ -93,14 +100,21 @@ where
for _ in 0..self.config.generations { for _ in 0..self.config.generations {
// Phase 1: parent selection + variation (random tournament on // Phase 1: parent selection + variation (random tournament on
// a fitness-by-HV-estimate proxy). // a fitness-by-HV-estimate proxy).
let fitness = let fitness = hype_fitness(
hype_fitness(&population, &objectives, &reference, self.config.mc_samples, &mut rng); &population,
&objectives,
&reference,
self.config.mc_samples,
&mut rng,
);
let mut offspring_decisions: Vec<P::Decision> = Vec::with_capacity(n); let mut offspring_decisions: Vec<P::Decision> = Vec::with_capacity(n);
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = binary_tournament(&fitness, &mut rng); let p1 = binary_tournament(&fitness, &mut rng);
let p2 = binary_tournament(&fitness, &mut rng); let p2 = binary_tournament(&fitness, &mut rng);
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "Hype variation returned no children"); assert!(!children.is_empty(), "Hype variation returned no children");
for child in children { for child in children {
@@ -140,8 +154,13 @@ where
// by largest HV contribution. // by largest HV contribution.
let pool: Vec<&Candidate<P::Decision>> = let pool: Vec<&Candidate<P::Decision>> =
splitting.iter().map(|&i| &combined[i]).collect(); splitting.iter().map(|&i| &combined[i]).collect();
let contributions = let contributions = estimate_contributions(
estimate_contributions(&pool, &objectives, &reference, self.config.mc_samples, &mut rng); &pool,
&objectives,
&reference,
self.config.mc_samples,
&mut rng,
);
let mut order: Vec<usize> = (0..splitting.len()).collect(); let mut order: Vec<usize> = (0..splitting.len()).collect();
order.sort_by(|&a, &b| { order.sort_by(|&a, &b| {
contributions[b] contributions[b]
@@ -154,7 +173,10 @@ where
} }
// Materialize the next generation. // Materialize the next generation.
population = keep_indices.into_iter().map(|i| combined[i].clone()).collect(); population = keep_indices
.into_iter()
.map(|i| combined[i].clone())
.collect();
} }
let front = pareto_front(&population, &objectives); let front = pareto_front(&population, &objectives);
@@ -321,10 +343,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+22 -11
View File
@@ -31,7 +31,12 @@ pub struct HyperbandConfig {
impl Default for HyperbandConfig { impl Default for HyperbandConfig {
fn default() -> Self { fn default() -> Self {
Self { max_budget: 81.0, eta: 3.0, max_brackets: 5, seed: 42 } Self {
max_budget: 81.0,
eta: 3.0,
max_brackets: 5,
seed: 42,
}
} }
} }
@@ -66,7 +71,11 @@ where
{ {
/// Construct a `Hyperband`. /// Construct a `Hyperband`.
pub fn new(config: HyperbandConfig, initializer: I) -> Self { pub fn new(config: HyperbandConfig, initializer: I) -> Self {
Self { config, initializer, _marker: std::marker::PhantomData } Self {
config,
initializer,
_marker: std::marker::PhantomData,
}
} }
/// Run Hyperband on a multi-fidelity problem, returning the standard /// Run Hyperband on a multi-fidelity problem, returning the standard
@@ -75,9 +84,15 @@ where
where where
P: PartialProblem<Decision = D>, P: PartialProblem<Decision = D>,
{ {
assert!(self.config.max_budget > 0.0, "Hyperband max_budget must be > 0"); assert!(
self.config.max_budget > 0.0,
"Hyperband max_budget must be > 0"
);
assert!(self.config.eta > 1.0, "Hyperband eta must be > 1"); assert!(self.config.eta > 1.0, "Hyperband eta must be > 1");
assert!(self.config.max_brackets >= 1, "Hyperband max_brackets must be >= 1"); assert!(
self.config.max_brackets >= 1,
"Hyperband max_brackets must be >= 1"
);
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!( assert!(
objectives.is_single_objective(), objectives.is_single_objective(),
@@ -97,9 +112,8 @@ where
// Brackets are indexed s = s_max, s_max - 1, ..., 0. // Brackets are indexed s = s_max, s_max - 1, ..., 0.
for s in (0..=s_max).rev() { for s in (0..=s_max).rev() {
let s_f = s as f64; let s_f = s as f64;
let n = ((s_max as f64 + 1.0) / (s_f + 1.0) let n =
* self.config.eta.powf(s_f)) ((s_max as f64 + 1.0) / (s_f + 1.0) * self.config.eta.powf(s_f)).ceil() as usize;
.ceil() as usize;
let r = self.config.max_budget / self.config.eta.powf(s_f); let r = self.config.max_budget / self.config.eta.powf(s_f);
// Sample n configurations. // Sample n configurations.
@@ -268,10 +282,7 @@ mod tests {
impl PartialProblem for MultiObj { impl PartialProblem for MultiObj {
type Decision = Vec<f64>; type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace { fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("a"), Objective::minimize("b")])
Objective::minimize("a"),
Objective::minimize("b"),
])
} }
fn evaluate_at_budget(&self, _: &Vec<f64>, _: f64) -> Evaluation { fn evaluate_at_budget(&self, _: &Vec<f64>, _: f64) -> Evaluation {
Evaluation::new(vec![0.0, 0.0]) Evaluation::new(vec![0.0, 0.0])
+42 -15
View File
@@ -27,7 +27,12 @@ pub struct IbeaConfig {
impl Default for IbeaConfig { impl Default for IbeaConfig {
fn default() -> Self { fn default() -> Self {
Self { population_size: 100, generations: 250, kappa: 0.05, seed: 42 } Self {
population_size: 100,
generations: 250,
kappa: 0.05,
seed: 42,
}
} }
} }
@@ -45,7 +50,11 @@ pub struct Ibea<I, V> {
impl<I, V> Ibea<I, V> { impl<I, V> Ibea<I, V> {
/// Construct an `Ibea` optimizer. /// Construct an `Ibea` optimizer.
pub fn new(config: IbeaConfig, initializer: I, variation: V) -> Self { pub fn new(config: IbeaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -57,7 +66,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "Ibea population_size must be > 0"); assert!(
self.config.population_size > 0,
"Ibea population_size must be > 0"
);
assert!(self.config.kappa > 0.0, "Ibea kappa must be > 0"); assert!(self.config.kappa > 0.0, "Ibea kappa must be > 0");
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
@@ -76,7 +88,10 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = binary_tournament(&fitness, &mut rng); let p1 = binary_tournament(&fitness, &mut rng);
let p2 = binary_tournament(&fitness, &mut rng); let p2 = binary_tournament(&fitness, &mut rng);
let parents = vec![population[p1].decision.clone(), population[p2].decision.clone()]; let parents = vec![
population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "Ibea variation returned no children"); assert!(!children.is_empty(), "Ibea variation returned no children");
for child in children { for child in children {
@@ -204,11 +219,7 @@ fn environmental_selection<D: Clone>(
} }
/// Compute IBEA fitness without mutating, for use in tournament selection. /// Compute IBEA fitness without mutating, for use in tournament selection.
fn compute_fitness<D>( fn compute_fitness<D>(pool: &[Candidate<D>], objectives: &ObjectiveSpace, kappa: f64) -> Vec<f64> {
pool: &[Candidate<D>],
objectives: &ObjectiveSpace,
kappa: f64,
) -> Vec<f64> {
if pool.is_empty() { if pool.is_empty() {
return Vec::new(); return Vec::new();
} }
@@ -284,7 +295,12 @@ mod tests {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
Ibea::new( Ibea::new(
IbeaConfig { population_size: 20, generations: 15, kappa: 0.05, seed }, IbeaConfig {
population_size: 20,
generations: 15,
kappa: 0.05,
seed,
},
initializer, initializer,
variation, variation,
) )
@@ -304,10 +320,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
@@ -321,7 +343,12 @@ mod tests {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
let mut opt = Ibea::new( let mut opt = Ibea::new(
IbeaConfig { population_size: 0, generations: 1, kappa: 0.05, seed: 0 }, IbeaConfig {
population_size: 0,
generations: 1,
kappa: 0.05,
seed: 0,
},
initializer, initializer,
variation, variation,
); );
+34 -15
View File
@@ -26,7 +26,11 @@ pub struct KneaConfig {
impl Default for KneaConfig { impl Default for KneaConfig {
fn default() -> Self { fn default() -> Self {
Self { population_size: 100, generations: 250, seed: 42 } Self {
population_size: 100,
generations: 250,
seed: 42,
}
} }
} }
@@ -48,7 +52,11 @@ pub struct Knea<I, V> {
impl<I, V> Knea<I, V> { impl<I, V> Knea<I, V> {
/// Construct a `Knea`. /// Construct a `Knea`.
pub fn new(config: KneaConfig, initializer: I, variation: V) -> Self { pub fn new(config: KneaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -60,7 +68,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "Knea population_size must be > 0"); assert!(
self.config.population_size > 0,
"Knea population_size must be > 0"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
@@ -75,8 +86,10 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "Knea variation returned no children"); assert!(!children.is_empty(), "Knea variation returned no children");
for child in children { for child in children {
@@ -191,11 +204,7 @@ fn environmental_selection<D: Clone>(
/// Perpendicular distance from `point` to the hyperplane through the M /// Perpendicular distance from `point` to the hyperplane through the M
/// extreme points (indices into `oriented`). /// extreme points (indices into `oriented`).
fn perpendicular_distance( fn perpendicular_distance(point: &[f64], extremes: &[usize], oriented: &[Vec<f64>]) -> f64 {
point: &[f64],
extremes: &[usize],
oriented: &[Vec<f64>],
) -> f64 {
let m = point.len(); let m = point.len();
if extremes.len() < m { if extremes.len() < m {
// Degenerate: just return the L2 norm relative to first extreme. // Degenerate: just return the L2 norm relative to first extreme.
@@ -237,7 +246,11 @@ mod tests {
mutation: PolynomialMutation::new(bounds, 20.0, 1.0), mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}; };
Knea::new( Knea::new(
KneaConfig { population_size: 20, generations: 15, seed }, KneaConfig {
population_size: 20,
generations: 15,
seed,
},
initializer, initializer,
variation, variation,
) )
@@ -256,10 +269,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
} }
+1 -1
View File
@@ -22,8 +22,8 @@ pub mod nsga3;
pub mod one_plus_one_es; pub mod one_plus_one_es;
pub mod paes; pub mod paes;
pub(crate) mod parallel_eval; pub(crate) mod parallel_eval;
pub mod pesa2;
pub mod particle_swarm; pub mod particle_swarm;
pub mod pesa2;
pub mod random_search; pub mod random_search;
pub mod rvea; pub mod rvea;
pub mod simulated_annealing; pub mod simulated_annealing;
+22 -12
View File
@@ -52,7 +52,11 @@ pub struct Moead<I, V> {
impl<I, V> Moead<I, V> { impl<I, V> Moead<I, V> {
/// Construct a `Moead` optimizer. /// Construct a `Moead` optimizer.
pub fn new(config: MoeadConfig, initializer: I, variation: V) -> Self { pub fn new(config: MoeadConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -119,7 +123,8 @@ where
.collect(); .collect();
for _ in 0..self.config.generations { for _ in 0..self.config.generations {
#[allow(clippy::needless_range_loop)] // Body indexes both `neighborhoods[i]` and `population[j]` via `nbh`. #[allow(clippy::needless_range_loop)]
// Body indexes both `neighborhoods[i]` and `population[j]` via `nbh`.
for i in 0..n { for i in 0..n {
// Pick two distinct parents from the neighborhood. // Pick two distinct parents from the neighborhood.
let nbh = &neighborhoods[i]; let nbh = &neighborhoods[i];
@@ -128,10 +133,15 @@ where
while p2 == p1 && nbh.len() > 1 { while p2 == p1 && nbh.len() > 1 {
p2 = *nbh.choose(&mut rng).unwrap(); p2 = *nbh.choose(&mut rng).unwrap();
} }
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "MOEA/D variation returned no children"); assert!(
!children.is_empty(),
"MOEA/D variation returned no children"
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
@@ -152,8 +162,7 @@ where
let g_cur = tchebycheff(&cur_oriented, &weights[j], &ideal); let g_cur = tchebycheff(&cur_oriented, &weights[j], &ideal);
let g_new = tchebycheff(&oriented_child, &weights[j], &ideal); let g_new = tchebycheff(&oriented_child, &weights[j], &ideal);
if g_new <= g_cur { if g_new <= g_cur {
population[j] = population[j] = Candidate::new(child_decision.clone(), child_eval.clone());
Candidate::new(child_decision.clone(), child_eval.clone());
} }
} }
} }
@@ -188,7 +197,11 @@ fn tchebycheff(oriented_objectives: &[f64], weight: &[f64], ideal: &[f64]) -> f6
} }
fn weight_distance(a: &[f64], b: &[f64]) -> f64 { fn weight_distance(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum::<f64>().sqrt() a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
} }
#[cfg(test)] #[cfg(test)]
@@ -201,10 +214,7 @@ mod tests {
fn make_optimizer( fn make_optimizer(
seed: u64, seed: u64,
) -> Moead< ) -> Moead<RealBounds, CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>> {
RealBounds,
CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>,
> {
let bounds = vec![(-5.0, 5.0)]; let bounds = vec![(-5.0, 5.0)];
let initializer = RealBounds::new(bounds.clone()); let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation { let variation = CompositeVariation {
+17 -10
View File
@@ -73,7 +73,10 @@ where
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.swarm_size >= 1, "Mopso swarm_size must be >= 1"); assert!(self.config.swarm_size >= 1, "Mopso swarm_size must be >= 1");
assert!(self.config.archive_size >= 1, "Mopso archive_size must be >= 1"); assert!(
self.config.archive_size >= 1,
"Mopso archive_size must be >= 1"
);
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!( assert!(
objectives.is_multi_objective(), objectives.is_multi_objective(),
@@ -128,9 +131,8 @@ where
let cognitive_term = let cognitive_term =
self.config.cognitive * r1 * (pbest_decisions[i][j] - positions[i][j]); self.config.cognitive * r1 * (pbest_decisions[i][j] - positions[i][j]);
let social_term = self.config.social * r2 * (leader[j] - positions[i][j]); let social_term = self.config.social * r2 * (leader[j] - positions[i][j]);
let mut v = self.config.inertia * velocities[i][j] let mut v =
+ cognitive_term self.config.inertia * velocities[i][j] + cognitive_term + social_term;
+ social_term;
if v > v_max[j] { if v > v_max[j] {
v = v_max[j]; v = v_max[j];
} else if v < -v_max[j] { } else if v < -v_max[j] {
@@ -148,8 +150,7 @@ where
// --- Phase 3: serial pbest + archive updates --- // --- Phase 3: serial pbest + archive updates ---
for (i, cand) in evaluated.iter().enumerate() { for (i, cand) in evaluated.iter().enumerate() {
let dominance = let dominance = pareto_compare(&cand.evaluation, &pbest_evals[i], &objectives);
pareto_compare(&cand.evaluation, &pbest_evals[i], &objectives);
let replace = match dominance { let replace = match dominance {
Dominance::Dominates => true, Dominance::Dominates => true,
Dominance::DominatedBy => false, Dominance::DominatedBy => false,
@@ -212,10 +213,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+9 -8
View File
@@ -69,7 +69,10 @@ where
P: Problem<Decision = Vec<f64>> + Sync, P: Problem<Decision = Vec<f64>> + Sync,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.reflection > 0.0, "NelderMead reflection must be > 0"); assert!(
self.config.reflection > 0.0,
"NelderMead reflection must be > 0"
);
assert!( assert!(
self.config.expansion > 1.0, self.config.expansion > 1.0,
"NelderMead expansion must be > 1", "NelderMead expansion must be > 1",
@@ -111,8 +114,7 @@ where
v[j] = (v[j] + step).clamp(lo, hi); v[j] = (v[j] + step).clamp(lo, hi);
vertices.push(v); vertices.push(v);
} }
let mut evals: Vec<Evaluation> = let mut evals: Vec<Evaluation> = vertices.iter().map(|v| problem.evaluate(v)).collect();
vertices.iter().map(|v| problem.evaluate(v)).collect();
let mut evaluations = evals.len(); let mut evaluations = evals.len();
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
@@ -141,8 +143,7 @@ where
if better(&r_eval, &evals[best_idx], direction) { if better(&r_eval, &evals[best_idx], direction) {
// Reflection beat the best — try expansion. // Reflection beat the best — try expansion.
let expanded = let expanded = self.reflect(&centroid, &vertices[worst_idx], self.config.expansion);
self.reflect(&centroid, &vertices[worst_idx], self.config.expansion);
let e_eval = problem.evaluate(&expanded); let e_eval = problem.evaluate(&expanded);
evaluations += 1; evaluations += 1;
if better(&e_eval, &r_eval, direction) { if better(&e_eval, &r_eval, direction) {
@@ -177,11 +178,11 @@ where
if idx == best_idx { if idx == best_idx {
continue; continue;
} }
#[allow(clippy::needless_range_loop)] // body indexes both vertices and best_pt. #[allow(clippy::needless_range_loop)]
// body indexes both vertices and best_pt.
for j in 0..n { for j in 0..n {
vertices[idx][j] = best_pt[j] vertices[idx][j] = best_pt[j]
+ self.config.shrinkage + self.config.shrinkage * (vertices[idx][j] - best_pt[j]);
* (vertices[idx][j] - best_pt[j]);
} }
// Clamp to bounds. // Clamp to bounds.
for (j, x) in vertices[idx].iter_mut().enumerate() { for (j, x) in vertices[idx].iter_mut().enumerate() {
+54 -15
View File
@@ -26,7 +26,11 @@ pub struct Nsga2Config {
impl Default for Nsga2Config { impl Default for Nsga2Config {
fn default() -> Self { fn default() -> Self {
Self { population_size: 100, generations: 250, seed: 42 } Self {
population_size: 100,
generations: 250,
seed: 42,
}
} }
} }
@@ -44,7 +48,11 @@ pub struct Nsga2<I, V> {
impl<I, V> Nsga2<I, V> { impl<I, V> Nsga2<I, V> {
/// Construct an `Nsga2` optimizer. /// Construct an `Nsga2` optimizer.
pub fn new(config: Nsga2Config, initializer: I, variation: V) -> Self { pub fn new(config: Nsga2Config, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -78,8 +86,7 @@ where
n, n,
"NSGA-II initializer must return exactly population_size decisions", "NSGA-II initializer must return exactly population_size decisions",
); );
let population: Vec<Candidate<P::Decision>> = let population: Vec<Candidate<P::Decision>> = evaluate_batch(problem, initial_decisions);
evaluate_batch(problem, initial_decisions);
let mut evaluations = population.len(); let mut evaluations = population.len();
// Annotate the starting population with rank and crowding so the first // Annotate the starting population with rank and crowding so the first
@@ -130,7 +137,9 @@ where
let dist = crowding_distance(&combined, front, &objectives); let dist = crowding_distance(&combined, front, &objectives);
let mut order: Vec<usize> = (0..front.len()).collect(); let mut order: Vec<usize> = (0..front.len()).collect();
order.sort_by(|&a, &b| { order.sort_by(|&a, &b| {
dist[b].partial_cmp(&dist[a]).unwrap_or(std::cmp::Ordering::Equal) dist[b]
.partial_cmp(&dist[a])
.unwrap_or(std::cmp::Ordering::Equal)
}); });
let needed = n - next.len(); let needed = n - next.len();
for &k in order.iter().take(needed) { for &k in order.iter().take(needed) {
@@ -178,7 +187,11 @@ fn annotate<D: Clone>(
population population
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, c)| Nsga2Entry { candidate: c, rank: rank[i], crowding_distance: dist[i] }) .map(|(i, c)| Nsga2Entry {
candidate: c,
rank: rank[i],
crowding_distance: dist[i],
})
.collect() .collect()
} }
@@ -212,7 +225,11 @@ mod tests {
#[test] #[test]
fn final_population_has_expected_size() { fn final_population_has_expected_size() {
let mut opt = Nsga2::new( let mut opt = Nsga2::new(
Nsga2Config { population_size: 20, generations: 5, seed: 1 }, Nsga2Config {
population_size: 20,
generations: 5,
seed: 1,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.3 }, GaussianMutation { sigma: 0.3 },
); );
@@ -224,7 +241,11 @@ mod tests {
#[test] #[test]
fn evaluation_count_at_least_initial_population() { fn evaluation_count_at_least_initial_population() {
let mut opt = Nsga2::new( let mut opt = Nsga2::new(
Nsga2Config { population_size: 16, generations: 3, seed: 2 }, Nsga2Config {
population_size: 16,
generations: 3,
seed: 2,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.3 }, GaussianMutation { sigma: 0.3 },
); );
@@ -236,21 +257,35 @@ mod tests {
#[test] #[test]
fn deterministic_with_same_seed() { fn deterministic_with_same_seed() {
let mut a = Nsga2::new( let mut a = Nsga2::new(
Nsga2Config { population_size: 16, generations: 5, seed: 99 }, Nsga2Config {
population_size: 16,
generations: 5,
seed: 99,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.2 }, GaussianMutation { sigma: 0.2 },
); );
let mut b = Nsga2::new( let mut b = Nsga2::new(
Nsga2Config { population_size: 16, generations: 5, seed: 99 }, Nsga2Config {
population_size: 16,
generations: 5,
seed: 99,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.2 }, GaussianMutation { sigma: 0.2 },
); );
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
@@ -258,7 +293,11 @@ mod tests {
#[should_panic(expected = "population_size must be greater than 0")] #[should_panic(expected = "population_size must be greater than 0")]
fn zero_population_size_panics() { fn zero_population_size_panics() {
let mut opt = Nsga2::new( let mut opt = Nsga2::new(
Nsga2Config { population_size: 0, generations: 1, seed: 0 }, Nsga2Config {
population_size: 0,
generations: 1,
seed: 0,
},
RealBounds::new(vec![(-1.0, 1.0)]), RealBounds::new(vec![(-1.0, 1.0)]),
GaussianMutation { sigma: 0.1 }, GaussianMutation { sigma: 0.1 },
); );
+26 -15
View File
@@ -56,7 +56,11 @@ pub struct Nsga3<I, V> {
impl<I, V> Nsga3<I, V> { impl<I, V> Nsga3<I, V> {
/// Construct an `Nsga3` optimizer. /// Construct an `Nsga3` optimizer.
pub fn new(config: Nsga3Config, initializer: I, variation: V) -> Self { pub fn new(config: Nsga3Config, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -99,8 +103,10 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!( assert!(
!children.is_empty(), !children.is_empty(),
@@ -117,11 +123,11 @@ where
evaluations += offspring.len(); evaluations += offspring.len();
// --- Combine + survival selection --- // --- Combine + survival selection ---
let mut combined: Vec<Candidate<P::Decision>> = let mut combined: Vec<Candidate<P::Decision>> = Vec::with_capacity(2 * n);
Vec::with_capacity(2 * n);
combined.extend(population); combined.extend(population);
combined.extend(offspring); combined.extend(offspring);
population = environmental_selection(&combined, &objectives, &reference_points, n, &mut rng); population =
environmental_selection(&combined, &objectives, &reference_points, n, &mut rng);
} }
let front = pareto_front(&population, &objectives); let front = pareto_front(&population, &objectives);
@@ -205,7 +211,9 @@ fn environmental_selection<D: Clone>(
let candidate_refs: Vec<usize> = (0..reference_points.len()) let candidate_refs: Vec<usize> = (0..reference_points.len())
.filter(|&j| !available_in_fl[j].is_empty() && niche_count[j] == min_count) .filter(|&j| !available_in_fl[j].is_empty() && niche_count[j] == min_count)
.collect(); .collect();
let &chosen_ref = candidate_refs.choose(rng).expect("non-empty by construction"); let &chosen_ref = candidate_refs
.choose(rng)
.expect("non-empty by construction");
let pool = &available_in_fl[chosen_ref]; let pool = &available_in_fl[chosen_ref];
let pick_local = if niche_count[chosen_ref] == 0 { let pick_local = if niche_count[chosen_ref] == 0 {
@@ -420,10 +428,7 @@ mod tests {
fn make_optimizer( fn make_optimizer(
seed: u64, seed: u64,
) -> Nsga3< ) -> Nsga3<RealBounds, CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>> {
RealBounds,
CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>,
> {
let bounds = vec![(-5.0, 5.0)]; let bounds = vec![(-5.0, 5.0)];
let initializer = RealBounds::new(bounds.clone()); let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation { let variation = CompositeVariation {
@@ -457,10 +462,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+5 -3
View File
@@ -68,7 +68,10 @@ where
P: Problem<Decision = Vec<f64>> + Sync, P: Problem<Decision = Vec<f64>> + Sync,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.initial_sigma > 0.0, "OnePlusOneEs initial_sigma must be > 0"); assert!(
self.config.initial_sigma > 0.0,
"OnePlusOneEs initial_sigma must be > 0"
);
assert!( assert!(
self.config.step_increase > 1.0, self.config.step_increase > 1.0,
"OnePlusOneEs step_increase must be > 1", "OnePlusOneEs step_increase must be > 1",
@@ -123,8 +126,7 @@ where
} }
// Apply one-fifth rule once we have a full window. // Apply one-fifth rule once we have a full window.
if window.len() == self.config.adaptation_period { if window.len() == self.config.adaptation_period {
let success_count: usize = let success_count: usize = window.iter().map(|&b| b as usize).sum();
window.iter().map(|&b| b as usize).sum();
let rate = success_count as f64 / window.len() as f64; let rate = success_count as f64 / window.len() as f64;
if rate > 0.2 { if rate > 0.2 {
sigma *= self.config.step_increase; sigma *= self.config.step_increase;
+34 -11
View File
@@ -23,7 +23,11 @@ pub struct PaesConfig {
impl Default for PaesConfig { impl Default for PaesConfig {
fn default() -> Self { fn default() -> Self {
Self { iterations: 1000, archive_size: 100, seed: 42 } Self {
iterations: 1000,
archive_size: 100,
seed: 42,
}
} }
} }
@@ -45,7 +49,11 @@ pub struct Paes<I, V> {
impl<I, V> Paes<I, V> { impl<I, V> Paes<I, V> {
/// Construct a `Paes` optimizer. /// Construct a `Paes` optimizer.
pub fn new(config: PaesConfig, initializer: I, variation: V) -> Self { pub fn new(config: PaesConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -74,15 +82,15 @@ where
let mut evaluations = 1usize; let mut evaluations = 1usize;
let mut archive = ParetoArchive::new(objectives.clone()); let mut archive = ParetoArchive::new(objectives.clone());
archive.insert(Candidate::new(current_decision.clone(), current_eval.clone())); archive.insert(Candidate::new(
current_decision.clone(),
current_eval.clone(),
));
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
let parents = vec![current_decision.clone()]; let parents = vec![current_decision.clone()];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!( assert!(!children.is_empty(), "PAES variation returned no children",);
!children.is_empty(),
"PAES variation returned no children",
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
@@ -103,7 +111,10 @@ where
} }
archive.insert(Candidate::new(child_decision, child_eval)); archive.insert(Candidate::new(child_decision, child_eval));
archive.insert(Candidate::new(current_decision.clone(), current_eval.clone())); archive.insert(Candidate::new(
current_decision.clone(),
current_eval.clone(),
));
archive.truncate(self.config.archive_size); archive.truncate(self.config.archive_size);
} }
@@ -129,7 +140,11 @@ mod tests {
#[test] #[test]
fn produces_at_least_one_candidate() { fn produces_at_least_one_candidate() {
let mut opt = Paes::new( let mut opt = Paes::new(
PaesConfig { iterations: 50, archive_size: 16, seed: 1 }, PaesConfig {
iterations: 50,
archive_size: 16,
seed: 1,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.3 }, GaussianMutation { sigma: 0.3 },
); );
@@ -141,7 +156,11 @@ mod tests {
#[test] #[test]
fn archive_size_respected() { fn archive_size_respected() {
let mut opt = Paes::new( let mut opt = Paes::new(
PaesConfig { iterations: 200, archive_size: 8, seed: 2 }, PaesConfig {
iterations: 200,
archive_size: 8,
seed: 2,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
GaussianMutation { sigma: 0.2 }, GaussianMutation { sigma: 0.2 },
); );
@@ -152,7 +171,11 @@ mod tests {
#[test] #[test]
fn single_objective_returns_best() { fn single_objective_returns_best() {
let mut opt = Paes::new( let mut opt = Paes::new(
PaesConfig { iterations: 200, archive_size: 8, seed: 3 }, PaesConfig {
iterations: 200,
archive_size: 8,
seed: 3,
},
RealBounds::new(vec![(-2.0, 2.0)]), RealBounds::new(vec![(-2.0, 2.0)]),
GaussianMutation { sigma: 0.1 }, GaussianMutation { sigma: 0.1 },
); );
+2 -3
View File
@@ -133,9 +133,8 @@ where
self.config.cognitive * r1 * (pbest_decisions[i][j] - positions[i][j]); self.config.cognitive * r1 * (pbest_decisions[i][j] - positions[i][j]);
let social_term = let social_term =
self.config.social * r2 * (gbest_decision[j] - positions[i][j]); self.config.social * r2 * (gbest_decision[j] - positions[i][j]);
let mut v = self.config.inertia * velocities[i][j] let mut v =
+ cognitive_term self.config.inertia * velocities[i][j] + cognitive_term + social_term;
+ social_term;
if v > v_max[j] { if v > v_max[j] {
v = v_max[j]; v = v_max[j];
} else if v < -v_max[j] { } else if v < -v_max[j] {
+46 -18
View File
@@ -60,7 +60,11 @@ pub struct PesaII<I, V> {
impl<I, V> PesaII<I, V> { impl<I, V> PesaII<I, V> {
/// Construct a `PesaII`. /// Construct a `PesaII`.
pub fn new(config: PesaIIConfig, initializer: I, variation: V) -> Self { pub fn new(config: PesaIIConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -72,9 +76,18 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "PesaII population_size must be > 0"); assert!(
assert!(self.config.archive_size > 0, "PesaII archive_size must be > 0"); self.config.population_size > 0,
assert!(self.config.grid_divisions >= 1, "PesaII grid_divisions must be >= 1"); "PesaII population_size must be > 0"
);
assert!(
self.config.archive_size > 0,
"PesaII archive_size must be > 0"
);
assert!(
self.config.grid_divisions >= 1,
"PesaII grid_divisions must be >= 1"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
@@ -95,7 +108,11 @@ where
for c in &internal { for c in &internal {
archive.insert(c.clone()); archive.insert(c.clone());
} }
truncate_by_grid(&mut archive, self.config.archive_size, self.config.grid_divisions); truncate_by_grid(
&mut archive,
self.config.archive_size,
self.config.grid_divisions,
);
for _ in 0..self.config.generations { for _ in 0..self.config.generations {
// Build grid + box counts on the archive. // Build grid + box counts on the archive.
@@ -106,9 +123,15 @@ where
while offspring.len() < n { while offspring.len() < n {
let p1 = region_tournament(&archive, &boxes, &counts, &mut rng); let p1 = region_tournament(&archive, &boxes, &counts, &mut rng);
let p2 = region_tournament(&archive, &boxes, &counts, &mut rng); let p2 = region_tournament(&archive, &boxes, &counts, &mut rng);
let parents = vec![archive.members()[p1].decision.clone(), archive.members()[p2].decision.clone()]; let parents = vec![
archive.members()[p1].decision.clone(),
archive.members()[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "PesaII variation returned no children"); assert!(
!children.is_empty(),
"PesaII variation returned no children"
);
for child in children { for child in children {
if offspring.len() >= n { if offspring.len() >= n {
break; break;
@@ -124,7 +147,11 @@ where
for c in &offspring { for c in &offspring {
archive.insert(c.clone()); archive.insert(c.clone());
} }
truncate_by_grid(&mut archive, self.config.archive_size, self.config.grid_divisions); truncate_by_grid(
&mut archive,
self.config.archive_size,
self.config.grid_divisions,
);
internal = offspring; internal = offspring;
} }
@@ -213,11 +240,7 @@ fn region_tournament<D: Clone>(
/// Truncate the archive to `max_size` by repeatedly evicting a uniform-random /// Truncate the archive to `max_size` by repeatedly evicting a uniform-random
/// member of the most-occupied grid box (PESA-II's standard approach). /// member of the most-occupied grid box (PESA-II's standard approach).
fn truncate_by_grid<D: Clone>( fn truncate_by_grid<D: Clone>(archive: &mut ParetoArchive<D>, max_size: usize, divisions: usize) {
archive: &mut ParetoArchive<D>,
max_size: usize,
divisions: usize,
) {
while archive.members().len() > max_size { while archive.members().len() > max_size {
let objectives = archive.objectives.clone(); let objectives = archive.objectives.clone();
let (boxes, counts) = build_grid(archive, &objectives, divisions); let (boxes, counts) = build_grid(archive, &objectives, divisions);
@@ -291,10 +314,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
@@ -320,5 +349,4 @@ mod tests {
); );
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
} }
+27 -6
View File
@@ -25,7 +25,11 @@ pub struct RandomSearchConfig {
impl Default for RandomSearchConfig { impl Default for RandomSearchConfig {
fn default() -> Self { fn default() -> Self {
Self { iterations: 100, batch_size: 1, seed: 42 } Self {
iterations: 100,
batch_size: 1,
seed: 42,
}
} }
} }
@@ -45,7 +49,10 @@ pub struct RandomSearch<I> {
impl<I> RandomSearch<I> { impl<I> RandomSearch<I> {
/// Construct a `RandomSearch` from its config and initializer. /// Construct a `RandomSearch` from its config and initializer.
pub fn new(config: RandomSearchConfig, initializer: I) -> Self { pub fn new(config: RandomSearchConfig, initializer: I) -> Self {
Self { config, initializer } Self {
config,
initializer,
}
} }
} }
@@ -62,7 +69,9 @@ where
let mut evaluations = 0usize; let mut evaluations = 0usize;
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
let decisions = self.initializer.initialize(self.config.batch_size, &mut rng); let decisions = self
.initializer
.initialize(self.config.batch_size, &mut rng);
evaluations += decisions.len(); evaluations += decisions.len();
all.extend(evaluate_batch(problem, decisions)); all.extend(evaluate_batch(problem, decisions));
} }
@@ -88,7 +97,11 @@ mod tests {
#[test] #[test]
fn evaluation_count_matches_iterations_times_batch() { fn evaluation_count_matches_iterations_times_batch() {
let mut opt = RandomSearch::new( let mut opt = RandomSearch::new(
RandomSearchConfig { iterations: 30, batch_size: 4, seed: 1 }, RandomSearchConfig {
iterations: 30,
batch_size: 4,
seed: 1,
},
RealBounds::new(vec![(-2.0, 2.0)]), RealBounds::new(vec![(-2.0, 2.0)]),
); );
let r = opt.run(&Sphere1D); let r = opt.run(&Sphere1D);
@@ -100,7 +113,11 @@ mod tests {
#[test] #[test]
fn pareto_front_non_empty_for_multi_objective() { fn pareto_front_non_empty_for_multi_objective() {
let mut opt = RandomSearch::new( let mut opt = RandomSearch::new(
RandomSearchConfig { iterations: 50, batch_size: 1, seed: 42 }, RandomSearchConfig {
iterations: 50,
batch_size: 1,
seed: 42,
},
RealBounds::new(vec![(-5.0, 5.0)]), RealBounds::new(vec![(-5.0, 5.0)]),
); );
let r = opt.run(&SchafferN1); let r = opt.run(&SchafferN1);
@@ -112,7 +129,11 @@ mod tests {
#[test] #[test]
fn single_objective_returns_best() { fn single_objective_returns_best() {
let mut opt = RandomSearch::new( let mut opt = RandomSearch::new(
RandomSearchConfig { iterations: 100, batch_size: 1, seed: 7 }, RandomSearchConfig {
iterations: 100,
batch_size: 1,
seed: 7,
},
RealBounds::new(vec![(-1.0, 1.0)]), RealBounds::new(vec![(-1.0, 1.0)]),
); );
let r = opt.run(&Sphere1D); let r = opt.run(&Sphere1D);
+44 -17
View File
@@ -54,7 +54,11 @@ pub struct Rvea<I, V> {
impl<I, V> Rvea<I, V> { impl<I, V> Rvea<I, V> {
/// Construct an `Rvea`. /// Construct an `Rvea`.
pub fn new(config: RveaConfig, initializer: I, variation: V) -> Self { pub fn new(config: RveaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -66,14 +70,20 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "Rvea population_size must be > 0"); assert!(
self.config.population_size > 0,
"Rvea population_size must be > 0"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
let m = objectives.len(); let m = objectives.len();
// Reference vectors normalized to unit norm. // Reference vectors normalized to unit norm.
let raw_refs = das_dennis(m, self.config.reference_divisions); let raw_refs = das_dennis(m, self.config.reference_divisions);
let references: Vec<Vec<f64>> = raw_refs.into_iter().map(unit_normalize).collect(); let references: Vec<Vec<f64>> = raw_refs.into_iter().map(unit_normalize).collect();
assert!(!references.is_empty(), "Rvea: no reference vectors generated"); assert!(
!references.is_empty(),
"Rvea: no reference vectors generated"
);
// Smallest angle between any two reference vectors — used to scale // Smallest angle between any two reference vectors — used to scale
// the APD penalty term. // the APD penalty term.
@@ -91,8 +101,10 @@ where
while offspring_decisions.len() < n { while offspring_decisions.len() < n {
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "Rvea variation returned no children"); assert!(!children.is_empty(), "Rvea variation returned no children");
for child in children { for child in children {
@@ -126,7 +138,11 @@ where
.iter() .iter()
.map(|c| { .map(|c| {
let oriented = objectives.as_minimization(&c.evaluation.objectives); let oriented = objectives.as_minimization(&c.evaluation.objectives);
oriented.iter().enumerate().map(|(k, v)| v - ideal[k]).collect() oriented
.iter()
.enumerate()
.map(|(k, v)| v - ideal[k])
.collect()
}) })
.collect(); .collect();
@@ -157,22 +173,23 @@ where
} }
} }
let mut next: Vec<Candidate<P::Decision>> = let mut next: Vec<Candidate<P::Decision>> = keep
keep.into_iter().flatten().map(|(i, _)| combined[i].clone()).collect(); .into_iter()
.flatten()
.map(|(i, _)| combined[i].clone())
.collect();
// If we ended up with fewer than n (some references unfilled), // If we ended up with fewer than n (some references unfilled),
// backfill with the lowest-APD remaining candidates. // backfill with the lowest-APD remaining candidates.
if next.len() < n { if next.len() < n {
let mut all_apds: Vec<(usize, f64)> = (0..combined.len()) let mut all_apds: Vec<(usize, f64)> = (0..combined.len())
.map(|i| { .map(|i| {
let length: f64 = let length: f64 = translated[i].iter().map(|v| v * v).sum::<f64>().sqrt();
translated[i].iter().map(|v| v * v).sum::<f64>().sqrt();
let theta_max_safe = theta_max.max(1e-12); let theta_max_safe = theta_max.max(1e-12);
let penalty = 1.0 + (m_dim as f64) * alpha_t * (angles[i] / theta_max_safe); let penalty = 1.0 + (m_dim as f64) * alpha_t * (angles[i] / theta_max_safe);
(i, penalty * length) (i, penalty * length)
}) })
.collect(); .collect();
all_apds all_apds.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
for (i, _) in all_apds { for (i, _) in all_apds {
if next.len() >= n { if next.len() >= n {
break; break;
@@ -246,7 +263,11 @@ fn smallest_neighbor_angle(references: &[Vec<f64>]) -> f64 {
} }
} }
} }
if !min_angle.is_finite() { std::f64::consts::FRAC_PI_4 } else { min_angle } if !min_angle.is_finite() {
std::f64::consts::FRAC_PI_4
} else {
min_angle
}
} }
#[cfg(test)] #[cfg(test)]
@@ -292,10 +313,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+9 -2
View File
@@ -54,7 +54,11 @@ pub struct SimulatedAnnealing<I, V> {
impl<I, V> SimulatedAnnealing<I, V> { impl<I, V> SimulatedAnnealing<I, V> {
/// Construct a `SimulatedAnnealing`. /// Construct a `SimulatedAnnealing`.
pub fn new(config: SimulatedAnnealingConfig, initializer: I, variation: V) -> Self { pub fn new(config: SimulatedAnnealingConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -109,7 +113,10 @@ where
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
let parents = vec![current_decision.clone()]; let parents = vec![current_decision.clone()];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "SimulatedAnnealing variation returned no children"); assert!(
!children.is_empty(),
"SimulatedAnnealing variation returned no children"
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
+27 -10
View File
@@ -62,7 +62,11 @@ pub struct SmsEmoa<I, V> {
impl<I, V> SmsEmoa<I, V> { impl<I, V> SmsEmoa<I, V> {
/// Construct a `SmsEmoa`. /// Construct a `SmsEmoa`.
pub fn new(config: SmsEmoaConfig, initializer: I, variation: V) -> Self { pub fn new(config: SmsEmoaConfig, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -74,7 +78,10 @@ where
V: Variation<P::Decision>, V: Variation<P::Decision>,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size > 0, "SmsEmoa population_size must be > 0"); assert!(
self.config.population_size > 0,
"SmsEmoa population_size must be > 0"
);
let n = self.config.population_size; let n = self.config.population_size;
let objectives = problem.objectives(); let objectives = problem.objectives();
assert_eq!( assert_eq!(
@@ -100,10 +107,15 @@ where
// --- One offspring (steady-state) --- // --- One offspring (steady-state) ---
let p1 = rng.random_range(0..population.len()); let p1 = rng.random_range(0..population.len());
let p2 = rng.random_range(0..population.len()); let p2 = rng.random_range(0..population.len());
let parents = let parents = vec![
vec![population[p1].decision.clone(), population[p2].decision.clone()]; population[p1].decision.clone(),
population[p2].decision.clone(),
];
let children = self.variation.vary(&parents, &mut rng); let children = self.variation.vary(&parents, &mut rng);
assert!(!children.is_empty(), "SmsEmoa variation returned no children"); assert!(
!children.is_empty(),
"SmsEmoa variation returned no children"
);
let child_decision = children.into_iter().next().unwrap(); let child_decision = children.into_iter().next().unwrap();
let child_eval = problem.evaluate(&child_decision); let child_eval = problem.evaluate(&child_decision);
evaluations += 1; evaluations += 1;
@@ -212,10 +224,16 @@ mod tests {
let mut b = make_optimizer(99); let mut b = make_optimizer(99);
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
@@ -263,4 +281,3 @@ mod tests {
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
} }
+8 -4
View File
@@ -76,7 +76,10 @@ where
self.config.population_size >= 2, self.config.population_size >= 2,
"SeparableNes population_size must be >= 2", "SeparableNes population_size must be >= 2",
); );
assert!(self.config.initial_sigma > 0.0, "SeparableNes initial_sigma must be > 0"); assert!(
self.config.initial_sigma > 0.0,
"SeparableNes initial_sigma must be > 0"
);
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!( assert!(
objectives.is_single_objective(), objectives.is_single_objective(),
@@ -97,9 +100,10 @@ where
let mut sigma = vec![self.config.initial_sigma; n]; let mut sigma = vec![self.config.initial_sigma; n];
// Default sigma learning rate (Wierstra et al. 2014, Eq. 11). // Default sigma learning rate (Wierstra et al. 2014, Eq. 11).
let eta_sigma = self.config.sigma_learning_rate.unwrap_or_else(|| { let eta_sigma = self
(3.0 + (n as f64).ln()) / (5.0 * (n as f64).sqrt()) .config
}); .sigma_learning_rate
.unwrap_or_else(|| (3.0 + (n as f64).ln()) / (5.0 * (n as f64).sqrt()));
let eta_mean = self.config.mean_learning_rate; let eta_mean = self.config.mean_learning_rate;
// Rank utilities — the standard NES weighting: // Rank utilities — the standard NES weighting:
+43 -14
View File
@@ -28,7 +28,12 @@ pub struct Spea2Config {
impl Default for Spea2Config { impl Default for Spea2Config {
fn default() -> Self { fn default() -> Self {
Self { population_size: 100, archive_size: 100, generations: 250, seed: 42 } Self {
population_size: 100,
archive_size: 100,
generations: 250,
seed: 42,
}
} }
} }
@@ -46,7 +51,11 @@ pub struct Spea2<I, V> {
impl<I, V> Spea2<I, V> { impl<I, V> Spea2<I, V> {
/// Construct a `Spea2` optimizer. /// Construct a `Spea2` optimizer.
pub fn new(config: Spea2Config, initializer: I, variation: V) -> Self { pub fn new(config: Spea2Config, initializer: I, variation: V) -> Self {
Self { config, initializer, variation } Self {
config,
initializer,
variation,
}
} }
} }
@@ -190,7 +199,11 @@ fn compute_fitness<D>(pool: &[Candidate<D>], objectives: &ObjectiveSpace) -> Vec
} }
fn euclidean(a: &[f64], b: &[f64]) -> f64 { fn euclidean(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum::<f64>().sqrt() a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
} }
/// Build the next archive of exactly `target_size` members. /// Build the next archive of exactly `target_size` members.
@@ -213,10 +226,11 @@ fn build_archive<D: Clone>(
if nondom.len() < target_size { if nondom.len() < target_size {
// Fill from dominated members ordered by ascending fitness. // Fill from dominated members ordered by ascending fitness.
let mut dominated: Vec<usize> = let mut dominated: Vec<usize> = (0..pool.len()).filter(|&i| fitness[i] >= 1.0).collect();
(0..pool.len()).filter(|&i| fitness[i] >= 1.0).collect();
dominated.sort_by(|&a, &b| { dominated.sort_by(|&a, &b| {
fitness[a].partial_cmp(&fitness[b]).unwrap_or(std::cmp::Ordering::Equal) fitness[a]
.partial_cmp(&fitness[b])
.unwrap_or(std::cmp::Ordering::Equal)
}); });
let needed = target_size - nondom.len(); let needed = target_size - nondom.len();
nondom.extend(dominated.into_iter().take(needed)); nondom.extend(dominated.into_iter().take(needed));
@@ -244,8 +258,7 @@ fn build_archive<D: Clone>(
} }
neighbor_dists[i].push(euclidean(&oriented[i], &oriented[j])); neighbor_dists[i].push(euclidean(&oriented[i], &oriented[j]));
} }
neighbor_dists[i] neighbor_dists[i].sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
} }
// Find the alive member whose neighbor-distance vector is lex-smallest. // Find the alive member whose neighbor-distance vector is lex-smallest.
let mut victim = usize::MAX; let mut victim = usize::MAX;
@@ -263,7 +276,11 @@ fn build_archive<D: Clone>(
.zip(neighbor_dists[victim].iter()) .zip(neighbor_dists[victim].iter())
.find_map(|(a, b)| { .find_map(|(a, b)| {
let c = a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal); let c = a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
if c != std::cmp::Ordering::Equal { Some(c) } else { None } if c != std::cmp::Ordering::Equal {
Some(c)
} else {
None
}
}) })
.unwrap_or(std::cmp::Ordering::Equal); .unwrap_or(std::cmp::Ordering::Equal);
if cmp == std::cmp::Ordering::Less { if cmp == std::cmp::Ordering::Less {
@@ -277,7 +294,13 @@ fn build_archive<D: Clone>(
nondom nondom
.into_iter() .into_iter()
.enumerate() .enumerate()
.filter_map(|(local, idx)| if alive[local] { Some(pool[idx].clone()) } else { None }) .filter_map(|(local, idx)| {
if alive[local] {
Some(pool[idx].clone())
} else {
None
}
})
.collect() .collect()
} }
@@ -354,10 +377,16 @@ mod tests {
let mut b = make(); let mut b = make();
let ra = a.run(&SchafferN1); let ra = a.run(&SchafferN1);
let rb = b.run(&SchafferN1); let rb = b.run(&SchafferN1);
let oa: Vec<Vec<f64>> = let oa: Vec<Vec<f64>> = ra
ra.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .pareto_front
let ob: Vec<Vec<f64>> = .iter()
rb.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect(); .map(|c| c.evaluation.objectives.clone())
.collect();
let ob: Vec<Vec<f64>> = rb
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
+25 -14
View File
@@ -25,7 +25,11 @@ pub struct TabuSearchConfig {
impl Default for TabuSearchConfig { impl Default for TabuSearchConfig {
fn default() -> Self { fn default() -> Self {
Self { iterations: 500, tabu_tenure: 16, seed: 42 } Self {
iterations: 500,
tabu_tenure: 16,
seed: 42,
}
} }
} }
@@ -62,7 +66,12 @@ where
{ {
/// Construct a `TabuSearch`. /// Construct a `TabuSearch`.
pub fn new(config: TabuSearchConfig, initializer: I, neighbors: N) -> Self { pub fn new(config: TabuSearchConfig, initializer: I, neighbors: N) -> Self {
Self { config, initializer, neighbors, _marker: std::marker::PhantomData } Self {
config,
initializer,
neighbors,
_marker: std::marker::PhantomData,
}
} }
} }
@@ -87,14 +96,18 @@ where
let mut rng = rng_from_seed(self.config.seed); let mut rng = rng_from_seed(self.config.seed);
let mut initial = self.initializer.initialize(1, &mut rng); let mut initial = self.initializer.initialize(1, &mut rng);
assert!(!initial.is_empty(), "TabuSearch initializer returned no decisions"); assert!(
!initial.is_empty(),
"TabuSearch initializer returned no decisions"
);
let mut current_decision = initial.remove(0); let mut current_decision = initial.remove(0);
let mut current_eval = problem.evaluate(&current_decision); let mut current_eval = problem.evaluate(&current_decision);
let mut best_decision = current_decision.clone(); let mut best_decision = current_decision.clone();
let mut best_eval = current_eval.clone(); let mut best_eval = current_eval.clone();
let mut evaluations = 1usize; let mut evaluations = 1usize;
let mut tabu_queue: VecDeque<P::Decision> = VecDeque::with_capacity(self.config.tabu_tenure); let mut tabu_queue: VecDeque<P::Decision> =
VecDeque::with_capacity(self.config.tabu_tenure);
let mut tabu_set: HashSet<P::Decision> = HashSet::new(); let mut tabu_set: HashSet<P::Decision> = HashSet::new();
for _ in 0..self.config.iterations { for _ in 0..self.config.iterations {
@@ -118,8 +131,7 @@ where
for (i, c) in candidates.iter().enumerate() { for (i, c) in candidates.iter().enumerate() {
let is_tabu = tabu_set.contains(c); let is_tabu = tabu_set.contains(c);
let aspires = is_tabu let aspires = is_tabu && better_than(&cand_evals[i], &best_eval, direction);
&& better_than(&cand_evals[i], &best_eval, direction);
if is_tabu && !aspires { if is_tabu && !aspires {
continue; continue;
} }
@@ -227,15 +239,16 @@ mod tests {
} }
} }
fn make_optimizer<F>( fn make_optimizer<F>(seed: u64, neighbors: F) -> TabuSearch<Vec<i32>, StartAtZero, F>
seed: u64,
neighbors: F,
) -> TabuSearch<Vec<i32>, StartAtZero, F>
where where
F: FnMut(&Vec<i32>, &mut Rng) -> Vec<Vec<i32>>, F: FnMut(&Vec<i32>, &mut Rng) -> Vec<Vec<i32>>,
{ {
TabuSearch::new( TabuSearch::new(
TabuSearchConfig { iterations: 50, tabu_tenure: 4, seed }, TabuSearchConfig {
iterations: 50,
tabu_tenure: 4,
seed,
},
StartAtZero, StartAtZero,
neighbors, neighbors,
) )
@@ -244,9 +257,7 @@ mod tests {
#[test] #[test]
fn finds_optimum_on_grid() { fn finds_optimum_on_grid() {
// Neighbors: ±1 of current value. // Neighbors: ±1 of current value.
let neighbors = |x: &Vec<i32>, _rng: &mut Rng| { let neighbors = |x: &Vec<i32>, _rng: &mut Rng| vec![vec![x[0] - 1], vec![x[0] + 1]];
vec![vec![x[0] - 1], vec![x[0] + 1]]
};
let mut opt = make_optimizer(1, neighbors); let mut opt = make_optimizer(1, neighbors);
let r = opt.run(&GridProblem); let r = opt.run(&GridProblem);
let best = r.best.unwrap(); let best = r.best.unwrap();
+10 -4
View File
@@ -27,7 +27,11 @@ pub struct TlboConfig {
impl Default for TlboConfig { impl Default for TlboConfig {
fn default() -> Self { fn default() -> Self {
Self { population_size: 30, generations: 200, seed: 42 } Self {
population_size: 30,
generations: 200,
seed: 42,
}
} }
} }
@@ -58,7 +62,10 @@ where
P: Problem<Decision = Vec<f64>> + Sync, P: Problem<Decision = Vec<f64>> + Sync,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size >= 2, "Tlbo population_size must be >= 2"); assert!(
self.config.population_size >= 2,
"Tlbo population_size must be >= 2"
);
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!( assert!(
objectives.is_single_objective(), objectives.is_single_objective(),
@@ -73,8 +80,7 @@ where
use crate::traits::Initializer as _; use crate::traits::Initializer as _;
self.bounds.initialize(n, &mut rng) self.bounds.initialize(n, &mut rng)
}; };
let mut evals: Vec<Evaluation> = let mut evals: Vec<Evaluation> = decisions.iter().map(|d| problem.evaluate(d)).collect();
decisions.iter().map(|d| problem.evaluate(d)).collect();
let mut evaluations = decisions.len(); let mut evaluations = decisions.len();
for _ in 0..self.config.generations { for _ in 0..self.config.generations {
+42 -8
View File
@@ -72,7 +72,10 @@ where
P: Problem<Decision = Vec<f64>> + Sync, P: Problem<Decision = Vec<f64>> + Sync,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.initial_samples >= 2, "Tpe initial_samples must be >= 2"); assert!(
self.config.initial_samples >= 2,
"Tpe initial_samples must be >= 2"
);
assert!( assert!(
self.config.good_fraction > 0.0 && self.config.good_fraction < 1.0, self.config.good_fraction > 0.0 && self.config.good_fraction < 1.0,
"Tpe good_fraction must be in (0, 1)", "Tpe good_fraction must be in (0, 1)",
@@ -81,7 +84,10 @@ where
self.config.candidate_samples >= 1, self.config.candidate_samples >= 1,
"Tpe candidate_samples must be >= 1", "Tpe candidate_samples must be >= 1",
); );
assert!(self.config.bandwidth_factor > 0.0, "Tpe bandwidth_factor must be > 0"); assert!(
self.config.bandwidth_factor > 0.0,
"Tpe bandwidth_factor must be > 0"
);
let objectives = problem.objectives(); let objectives = problem.objectives();
assert!( assert!(
objectives.is_single_objective(), objectives.is_single_objective(),
@@ -110,9 +116,27 @@ where
let mut best_x: Option<Vec<f64>> = None; let mut best_x: Option<Vec<f64>> = None;
let mut best_ratio = f64::NEG_INFINITY; let mut best_ratio = f64::NEG_INFINITY;
for _ in 0..self.config.candidate_samples { for _ in 0..self.config.candidate_samples {
let cand = sample_from_kde(&decisions, &good_idx, &self.bounds, self.config.bandwidth_factor, &mut rng); let cand = sample_from_kde(
let l = log_kde_density(&cand, &decisions, &good_idx, &self.bounds, self.config.bandwidth_factor); &decisions,
let g = log_kde_density(&cand, &decisions, &bad_idx, &self.bounds, self.config.bandwidth_factor); &good_idx,
&self.bounds,
self.config.bandwidth_factor,
&mut rng,
);
let l = log_kde_density(
&cand,
&decisions,
&good_idx,
&self.bounds,
self.config.bandwidth_factor,
);
let g = log_kde_density(
&cand,
&decisions,
&bad_idx,
&self.bounds,
self.config.bandwidth_factor,
);
let ratio = l - g; let ratio = l - g;
if ratio > best_ratio { if ratio > best_ratio {
best_ratio = ratio; best_ratio = ratio;
@@ -180,7 +204,13 @@ fn sample_uniform_in_bounds(bounds: &RealBounds, rng: &mut Rng) -> Vec<f64> {
bounds bounds
.bounds .bounds
.iter() .iter()
.map(|&(lo, hi)| if lo == hi { lo } else { lo + (hi - lo) * rng.random::<f64>() }) .map(|&(lo, hi)| {
if lo == hi {
lo
} else {
lo + (hi - lo) * rng.random::<f64>()
}
})
.collect() .collect()
} }
@@ -191,7 +221,9 @@ fn split_good_bad(targets: &[f64], good_fraction: f64) -> (Vec<usize>, Vec<usize
let n = targets.len(); let n = targets.len();
let mut order: Vec<usize> = (0..n).collect(); let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| { order.sort_by(|&a, &b| {
targets[a].partial_cmp(&targets[b]).unwrap_or(std::cmp::Ordering::Equal) targets[a]
.partial_cmp(&targets[b])
.unwrap_or(std::cmp::Ordering::Equal)
}); });
let n_good = ((n as f64) * good_fraction).round() as usize; let n_good = ((n as f64) * good_fraction).round() as usize;
let n_good = n_good.clamp(1, n.saturating_sub(1)); let n_good = n_good.clamp(1, n.saturating_sub(1));
@@ -288,7 +320,9 @@ fn scott_bandwidths(decisions: &[Vec<f64>], support: &[usize], factor: f64) -> V
*v /= denom; *v /= denom;
} }
let scott_n = (support.len() as f64).powf(-0.2); let scott_n = (support.len() as f64).powf(-0.2);
vars.into_iter().map(|v| factor * v.sqrt().max(1e-6) * scott_n).collect() vars.into_iter()
.map(|v| factor * v.sqrt().max(1e-6) * scott_n)
.collect()
} }
#[cfg(test)] #[cfg(test)]
+10 -6
View File
@@ -67,7 +67,10 @@ where
P: Problem<Decision = Vec<bool>> + Sync, P: Problem<Decision = Vec<bool>> + Sync,
{ {
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> { fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
assert!(self.config.population_size >= 2, "Umda population_size must be >= 2"); assert!(
self.config.population_size >= 2,
"Umda population_size must be >= 2"
);
assert!( assert!(
self.config.selected_size >= 1, self.config.selected_size >= 1,
"Umda selected_size must be >= 1", "Umda selected_size must be >= 1",
@@ -114,7 +117,11 @@ where
// --- Phase 1: select top μ members --- // --- Phase 1: select top μ members ---
let mut order: Vec<usize> = (0..population.len()).collect(); let mut order: Vec<usize> = (0..population.len()).collect();
order.sort_by(|&a, &b| { order.sort_by(|&a, &b| {
compare_so(&population[a].evaluation, &population[b].evaluation, direction) compare_so(
&population[a].evaluation,
&population[b].evaluation,
direction,
)
}); });
let selected: Vec<&Candidate<Vec<bool>>> = let selected: Vec<&Candidate<Vec<bool>>> =
order.iter().take(mu).map(|&i| &population[i]).collect(); order.iter().take(mu).map(|&i| &population[i]).collect();
@@ -228,10 +235,7 @@ mod tests {
type Decision = Vec<bool>; type Decision = Vec<bool>;
fn objectives(&self) -> ObjectiveSpace { fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("a"), Objective::minimize("b")])
Objective::minimize("a"),
Objective::minimize("b"),
])
} }
fn evaluate(&self, _x: &Vec<bool>) -> Evaluation { fn evaluate(&self, _x: &Vec<bool>) -> Evaluation {
+4 -1
View File
@@ -18,7 +18,10 @@ pub struct Candidate<D> {
impl<D> Candidate<D> { impl<D> Candidate<D> {
/// Pair a decision with its evaluation. /// Pair a decision with its evaluation.
pub fn new(decision: D, evaluation: Evaluation) -> Self { pub fn new(decision: D, evaluation: Evaluation) -> Self {
Self { decision, evaluation } Self {
decision,
evaluation,
}
} }
} }
+8 -2
View File
@@ -18,12 +18,18 @@ pub struct Evaluation {
impl Evaluation { impl Evaluation {
/// Build a feasible evaluation from objective values. /// Build a feasible evaluation from objective values.
pub fn new(objectives: Vec<f64>) -> Self { pub fn new(objectives: Vec<f64>) -> Self {
Self { objectives, constraint_violation: 0.0 } Self {
objectives,
constraint_violation: 0.0,
}
} }
/// Build an evaluation with a known total constraint violation. /// Build an evaluation with a known total constraint violation.
pub fn constrained(objectives: Vec<f64>, constraint_violation: f64) -> Self { pub fn constrained(objectives: Vec<f64>, constraint_violation: f64) -> Self {
Self { objectives, constraint_violation } Self {
objectives,
constraint_violation,
}
} }
/// Returns `true` when `constraint_violation <= 0.0`. /// Returns `true` when `constraint_violation <= 0.0`.
+9 -6
View File
@@ -26,12 +26,18 @@ pub struct Objective {
impl Objective { impl Objective {
/// Create a minimize objective with the given name. /// Create a minimize objective with the given name.
pub fn minimize(name: impl Into<String>) -> Self { pub fn minimize(name: impl Into<String>) -> Self {
Self { name: name.into(), direction: Direction::Minimize } Self {
name: name.into(),
direction: Direction::Minimize,
}
} }
/// Create a maximize objective with the given name. /// Create a maximize objective with the given name.
pub fn maximize(name: impl Into<String>) -> Self { pub fn maximize(name: impl Into<String>) -> Self {
Self { name: name.into(), direction: Direction::Maximize } Self {
name: name.into(),
direction: Direction::Maximize,
}
} }
} }
@@ -125,10 +131,7 @@ mod tests {
assert!(!single.is_empty()); assert!(!single.is_empty());
assert_eq!(single.len(), 1); assert_eq!(single.len(), 1);
let multi = ObjectiveSpace::new(vec![ let multi = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
Objective::minimize("f1"),
Objective::minimize("f2"),
]);
assert!(multi.is_multi_objective()); assert!(multi.is_multi_objective());
assert!(!multi.is_single_objective()); assert!(!multi.is_single_objective());
+7 -1
View File
@@ -31,7 +31,13 @@ impl<D> OptimizationResult<D> {
evaluations: usize, evaluations: usize,
generations: usize, generations: usize,
) -> Self { ) -> Self {
Self { population, pareto_front, best, evaluations, generations } Self {
population,
pareto_front,
best,
evaluations,
generations,
}
} }
/// The final population. /// The final population.
+4 -1
View File
@@ -21,7 +21,10 @@ pub(crate) fn symmetric_eigen(
max_sweeps: usize, max_sweeps: usize,
) -> (Vec<f64>, Vec<Vec<f64>>) { ) -> (Vec<f64>, Vec<Vec<f64>>) {
let n = matrix.len(); let n = matrix.len();
debug_assert!(matrix.iter().all(|row| row.len() == n), "matrix must be square"); debug_assert!(
matrix.iter().all(|row| row.len() == n),
"matrix must be square"
);
// Working copy of the matrix; converges to a diagonal of eigenvalues. // Working copy of the matrix; converges to a diagonal of eigenvalues.
let mut a: Vec<Vec<f64>> = matrix.to_vec(); let mut a: Vec<Vec<f64>> = matrix.to_vec();
+24 -23
View File
@@ -71,10 +71,7 @@ mod tests {
} }
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
#[test] #[test]
@@ -83,7 +80,11 @@ mod tests {
// Dominated region area = 4*4 - sum of "outside" rectangles // Dominated region area = 4*4 - sum of "outside" rectangles
// stripes: x∈[1,2] y∈[3,4]→1, x∈[2,3] y∈[2,4]→2, x∈[3,4] y∈[1,4]→3 → total dominated = 1+2+3 = 6. // stripes: x∈[1,2] y∈[3,4]→1, x∈[2,3] y∈[2,4]→2, x∈[3,4] y∈[1,4]→3 → total dominated = 1+2+3 = 6.
let s = space_min2(); let s = space_min2();
let front = [cand(vec![1.0, 3.0]), cand(vec![2.0, 2.0]), cand(vec![3.0, 1.0])]; let front = [
cand(vec![1.0, 3.0]),
cand(vec![2.0, 2.0]),
cand(vec![3.0, 1.0]),
];
let hv = hypervolume_2d(&front, &s, [4.0, 4.0]); let hv = hypervolume_2d(&front, &s, [4.0, 4.0]);
assert!((hv - 6.0).abs() < 1e-12, "expected 6.0, got {hv}"); assert!((hv - 6.0).abs() < 1e-12, "expected 6.0, got {hv}");
} }
@@ -156,7 +157,10 @@ pub fn hypervolume_nd<D>(
reference_point.len(), reference_point.len(),
"hypervolume_nd: ObjectiveSpace and reference_point must agree on dimension", "hypervolume_nd: ObjectiveSpace and reference_point must agree on dimension",
); );
assert!(!reference_point.is_empty(), "hypervolume_nd: dimension must be >= 1"); assert!(
!reference_point.is_empty(),
"hypervolume_nd: dimension must be >= 1"
);
if front.is_empty() { if front.is_empty() {
return 0.0; return 0.0;
@@ -193,9 +197,7 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
// 2-D HV via the same sweep used by hypervolume_2d. Inlined here // 2-D HV via the same sweep used by hypervolume_2d. Inlined here
// because we already have the points in oriented form. // because we already have the points in oriented form.
let mut sorted: Vec<&Vec<f64>> = points.iter().collect(); let mut sorted: Vec<&Vec<f64>> = points.iter().collect();
sorted.sort_by(|a, b| { sorted.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap_or(std::cmp::Ordering::Equal));
a[0].partial_cmp(&b[0]).unwrap_or(std::cmp::Ordering::Equal)
});
let mut area = 0.0; let mut area = 0.0;
let mut last_y = reference[1]; let mut last_y = reference[1];
for p in sorted { for p in sorted {
@@ -236,10 +238,7 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
for p in sorted.into_iter().rev() { for p in sorted.into_iter().rev() {
let depth = prev - p[last]; let depth = prev - p[last];
if depth > 0.0 && !active.is_empty() { if depth > 0.0 && !active.is_empty() {
let projected: Vec<Vec<f64>> = active let projected: Vec<Vec<f64>> = active.iter().map(|q| q[..last].to_vec()).collect();
.iter()
.map(|q| q[..last].to_vec())
.collect();
let nd = non_dominated_projection(&projected); let nd = non_dominated_projection(&projected);
total += depth * hso_recursive(&nd, &sub_reference); total += depth * hso_recursive(&nd, &sub_reference);
} }
@@ -259,7 +258,11 @@ fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
/// Drop dominated members of a projected point set. /// Drop dominated members of a projected point set.
fn non_dominated_projection(points: &[Vec<f64>]) -> Vec<Vec<f64>> { fn non_dominated_projection(points: &[Vec<f64>]) -> Vec<Vec<f64>> {
let m = if let Some(first) = points.first() { first.len() } else { return Vec::new(); }; let m = if let Some(first) = points.first() {
first.len()
} else {
return Vec::new();
};
let mut out: Vec<Vec<f64>> = Vec::new(); let mut out: Vec<Vec<f64>> = Vec::new();
'outer: for p in points { 'outer: for p in points {
// Skip if dominated by any kept point. // Skip if dominated by any kept point.
@@ -327,11 +330,12 @@ mod nd_tests {
#[test] #[test]
fn nd_matches_2d_on_known_case() { fn nd_matches_2d_on_known_case() {
let s = ObjectiveSpace::new(vec![ let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
Objective::minimize("f1"), let front = [
Objective::minimize("f2"), cand_n(vec![1.0, 3.0]),
]); cand_n(vec![2.0, 2.0]),
let front = [cand_n(vec![1.0, 3.0]), cand_n(vec![2.0, 2.0]), cand_n(vec![3.0, 1.0])]; cand_n(vec![3.0, 1.0]),
];
let hv2 = hypervolume_2d(&front, &s, [4.0, 4.0]); let hv2 = hypervolume_2d(&front, &s, [4.0, 4.0]);
let hvn = hypervolume_nd(&front, &s, &[4.0, 4.0]); let hvn = hypervolume_nd(&front, &s, &[4.0, 4.0]);
assert!((hv2 - hvn).abs() < 1e-12, "{hv2} vs {hvn}"); assert!((hv2 - hvn).abs() < 1e-12, "{hv2} vs {hvn}");
@@ -409,10 +413,7 @@ mod nd_tests {
#[test] #[test]
#[should_panic(expected = "must agree on dimension")] #[should_panic(expected = "must agree on dimension")]
fn nd_panics_on_dim_mismatch() { fn nd_panics_on_dim_mismatch() {
let s = ObjectiveSpace::new(vec![ let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
Objective::minimize("f1"),
Objective::minimize("f2"),
]);
let front = [cand_n(vec![1.0, 1.0])]; let front = [cand_n(vec![1.0, 1.0])];
let _ = hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]); let _ = hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]);
} }
+2 -6
View File
@@ -39,8 +39,7 @@ pub fn spacing<D>(front: &[Candidate<D>], objectives: &ObjectiveSpace) -> f64 {
} }
let mean = nearest.iter().sum::<f64>() / n as f64; let mean = nearest.iter().sum::<f64>() / n as f64;
let variance = let variance = nearest.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / n as f64;
nearest.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / n as f64;
variance.sqrt() variance.sqrt()
} }
@@ -55,10 +54,7 @@ mod tests {
} }
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
#[test] #[test]
+30 -10
View File
@@ -38,7 +38,11 @@ impl Initializer<Vec<f64>> for RealBounds {
for _ in 0..size { for _ in 0..size {
let mut decision = Vec::with_capacity(self.bounds.len()); let mut decision = Vec::with_capacity(self.bounds.len());
for &(lo, hi) in &self.bounds { for &(lo, hi) in &self.bounds {
let v = if lo == hi { lo } else { rng.random_range(lo..=hi) }; let v = if lo == hi {
lo
} else {
rng.random_range(lo..=hi)
};
decision.push(v); decision.push(v);
} }
out.push(decision); out.push(decision);
@@ -63,8 +67,7 @@ impl Variation<Vec<f64>> for GaussianMutation {
!parents.is_empty(), !parents.is_empty(),
"GaussianMutation requires at least one parent", "GaussianMutation requires at least one parent",
); );
let normal = let normal = Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
let mut child = parents[0].clone(); let mut child = parents[0].clone();
for x in child.iter_mut() { for x in child.iter_mut() {
*x += normal.sample(rng); *x += normal.sample(rng);
@@ -113,7 +116,11 @@ impl SimulatedBinaryCrossover {
(0.0..=1.0).contains(&per_variable_probability), (0.0..=1.0).contains(&per_variable_probability),
"SimulatedBinaryCrossover per_variable_probability must be in [0.0, 1.0]", "SimulatedBinaryCrossover per_variable_probability must be in [0.0, 1.0]",
); );
Self { bounds, eta, per_variable_probability } Self {
bounds,
eta,
per_variable_probability,
}
} }
} }
@@ -201,7 +208,11 @@ impl PolynomialMutation {
(0.0..=1.0).contains(&per_variable_probability), (0.0..=1.0).contains(&per_variable_probability),
"PolynomialMutation per_variable_probability must be in [0.0, 1.0]", "PolynomialMutation per_variable_probability must be in [0.0, 1.0]",
); );
Self { bounds, eta, per_variable_probability } Self {
bounds,
eta,
per_variable_probability,
}
} }
} }
@@ -257,7 +268,10 @@ impl BoundedGaussianMutation {
/// # Panics /// # Panics
/// If `sigma <= 0.0` or any bound has `lo > hi`. /// If `sigma <= 0.0` or any bound has `lo > hi`.
pub fn new(sigma: f64, bounds: Vec<(f64, f64)>) -> Self { pub fn new(sigma: f64, bounds: Vec<(f64, f64)>) -> Self {
assert!(sigma > 0.0, "BoundedGaussianMutation sigma must be positive"); assert!(
sigma > 0.0,
"BoundedGaussianMutation sigma must be positive"
);
for (i, &(lo, hi)) in bounds.iter().enumerate() { for (i, &(lo, hi)) in bounds.iter().enumerate() {
assert!( assert!(
lo <= hi, lo <= hi,
@@ -279,8 +293,7 @@ impl Variation<Vec<f64>> for BoundedGaussianMutation {
self.bounds.len(), self.bounds.len(),
"BoundedGaussianMutation parent length must match bounds length", "BoundedGaussianMutation parent length must match bounds length",
); );
let normal = let normal = Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
let mut child = parents[0].clone(); let mut child = parents[0].clone();
for (x, &(lo, hi)) in child.iter_mut().zip(self.bounds.iter()) { for (x, &(lo, hi)) in child.iter_mut().zip(self.bounds.iter()) {
*x = (*x + normal.sample(rng)).clamp(lo, hi); *x = (*x + normal.sample(rng)).clamp(lo, hi);
@@ -330,13 +343,20 @@ impl LevyMutation {
"LevyMutation bound at index {i} has lo > hi: ({lo}, {hi})", "LevyMutation bound at index {i} has lo > hi: ({lo}, {hi})",
); );
} }
Self { alpha, scale, bounds } Self {
alpha,
scale,
bounds,
}
} }
} }
impl Variation<Vec<f64>> for LevyMutation { impl Variation<Vec<f64>> for LevyMutation {
fn vary(&mut self, parents: &[Vec<f64>], rng: &mut Rng) -> Vec<Vec<f64>> { fn vary(&mut self, parents: &[Vec<f64>], rng: &mut Rng) -> Vec<Vec<f64>> {
assert!(!parents.is_empty(), "LevyMutation requires at least one parent"); assert!(
!parents.is_empty(),
"LevyMutation requires at least one parent"
);
let alpha = self.alpha; let alpha = self.alpha;
// Mantegna's algorithm σ for the numerator Normal: // Mantegna's algorithm σ for the numerator Normal:
// sigma_u = (Γ(1+α)·sin(π·α/2) / (Γ((1+α)/2)·α·2^((α-1)/2)))^(1/α) // sigma_u = (Γ(1+α)·sin(π·α/2) / (Γ((1+α)/2)·α·2^((α-1)/2)))^(1/α)
+5 -5
View File
@@ -21,7 +21,10 @@ pub struct ParetoArchive<D> {
impl<D: Clone> ParetoArchive<D> { impl<D: Clone> ParetoArchive<D> {
/// Build an empty archive against the given objective space. /// Build an empty archive against the given objective space.
pub fn new(objectives: ObjectiveSpace) -> Self { pub fn new(objectives: ObjectiveSpace) -> Self {
Self { members: Vec::new(), objectives } Self {
members: Vec::new(),
objectives,
}
} }
/// Insert a candidate, preserving the non-domination property. /// Insert a candidate, preserving the non-domination property.
@@ -85,10 +88,7 @@ mod tests {
use crate::core::objective::Objective; use crate::core::objective::Objective;
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
fn cand(decision: u32, obj: Vec<f64>) -> Candidate<u32> { fn cand(decision: u32, obj: Vec<f64>) -> Candidate<u32> {
+1 -4
View File
@@ -77,10 +77,7 @@ mod tests {
} }
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
#[test] #[test]
+2 -9
View File
@@ -29,11 +29,7 @@ pub enum Dominance {
/// `constraint_violation` dominates. /// `constraint_violation` dominates.
/// 3. Otherwise compare objective values after converting both to /// 3. Otherwise compare objective values after converting both to
/// minimization orientation via [`ObjectiveSpace::as_minimization`]. /// minimization orientation via [`ObjectiveSpace::as_minimization`].
pub fn pareto_compare( pub fn pareto_compare(a: &Evaluation, b: &Evaluation, objectives: &ObjectiveSpace) -> Dominance {
a: &Evaluation,
b: &Evaluation,
objectives: &ObjectiveSpace,
) -> Dominance {
let a_feasible = a.is_feasible(); let a_feasible = a.is_feasible();
let b_feasible = b.is_feasible(); let b_feasible = b.is_feasible();
match (a_feasible, b_feasible) { match (a_feasible, b_feasible) {
@@ -78,10 +74,7 @@ mod tests {
use crate::core::objective::Objective; use crate::core::objective::Objective;
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
#[test] #[test]
+1 -4
View File
@@ -69,10 +69,7 @@ mod tests {
} }
fn space_min2() -> ObjectiveSpace { fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
#[test] #[test]
+11 -2
View File
@@ -11,7 +11,10 @@
/// # Panics /// # Panics
/// If `num_objectives == 0`. /// If `num_objectives == 0`.
pub fn das_dennis(num_objectives: usize, divisions: usize) -> Vec<Vec<f64>> { pub fn das_dennis(num_objectives: usize, divisions: usize) -> Vec<Vec<f64>> {
assert!(num_objectives > 0, "das_dennis requires num_objectives >= 1"); assert!(
num_objectives > 0,
"das_dennis requires num_objectives >= 1"
);
let mut out = Vec::new(); let mut out = Vec::new();
let mut current = Vec::with_capacity(num_objectives); let mut current = Vec::with_capacity(num_objectives);
recurse(num_objectives, divisions, divisions, &mut current, &mut out); recurse(num_objectives, divisions, divisions, &mut current, &mut out);
@@ -34,7 +37,13 @@ fn recurse(
} }
for take in 0..=remaining_units { for take in 0..=remaining_units {
current.push(take); current.push(take);
recurse(remaining_axes - 1, remaining_units - take, total, current, out); recurse(
remaining_axes - 1,
remaining_units - take,
total,
current,
out,
);
current.pop(); current.pop();
} }
} }
+14 -17
View File
@@ -12,28 +12,25 @@ pub use crate::core::{
pub use crate::traits::{Initializer, Optimizer, Repair, Variation}; pub use crate::traits::{Initializer, Optimizer, Repair, Variation};
pub use crate::pareto::{ pub use crate::pareto::{
Dominance, ParetoArchive, best_candidate, crowding_distance, das_dennis, Dominance, ParetoArchive, best_candidate, crowding_distance, das_dennis, non_dominated_sort,
non_dominated_sort, pareto_compare, pareto_front, pareto_compare, pareto_front,
}; };
pub use crate::operators::{ pub use crate::operators::{
BitFlipMutation, BoundedGaussianMutation, ClampToBounds, CompositeVariation, BitFlipMutation, BoundedGaussianMutation, ClampToBounds, CompositeVariation, GaussianMutation,
GaussianMutation, LevyMutation, PolynomialMutation, ProjectToSimplex, RealBounds, LevyMutation, PolynomialMutation, ProjectToSimplex, RealBounds, SimulatedBinaryCrossover,
SimulatedBinaryCrossover, SwapMutation, SwapMutation,
}; };
pub use crate::algorithms::{ pub use crate::algorithms::{
AgeMoea, AgeMoeaConfig, AntColonyTsp, AntColonyTspConfig, BayesianOpt, AgeMoea, AgeMoeaConfig, AntColonyTsp, AntColonyTspConfig, BayesianOpt, BayesianOptConfig,
BayesianOptConfig, CmaEs, CmaEsConfig, DifferentialEvolution, CmaEs, CmaEsConfig, DifferentialEvolution, DifferentialEvolutionConfig, EpsilonMoea,
DifferentialEvolutionConfig, EpsilonMoea, EpsilonMoeaConfig, EpsilonMoeaConfig, GeneticAlgorithm, GeneticAlgorithmConfig, Grea, GreaConfig, HillClimber,
GeneticAlgorithm, GeneticAlgorithmConfig, Grea, GreaConfig, HillClimber, HillClimberConfig, Hype, HillClimberConfig, Hype, HypeConfig, Hyperband, HyperbandConfig, Ibea, IbeaConfig, IpopCmaEs,
HypeConfig, Hyperband, HyperbandConfig, Ibea, IbeaConfig, IpopCmaEs, IpopCmaEsConfig, IpopCmaEsConfig, Knea, KneaConfig, Moead, MoeadConfig, Mopso, MopsoConfig, NelderMead,
Knea, KneaConfig, Moead, MoeadConfig, Mopso, MopsoConfig, NelderMeadConfig, Nsga2, Nsga2Config, Nsga3, Nsga3Config, OnePlusOneEs, OnePlusOneEsConfig,
NelderMead, NelderMeadConfig, Nsga2, Paes, PaesConfig, ParticleSwarm, ParticleSwarmConfig, PesaII, PesaIIConfig, RandomSearch,
Nsga2Config, Nsga3, Nsga3Config, OnePlusOneEs, OnePlusOneEsConfig, Paes, PaesConfig, ParticleSwarm, PesaII, PesaIIConfig, RandomSearchConfig, Rvea, RveaConfig, SeparableNes, SeparableNesConfig, SimulatedAnnealing,
ParticleSwarmConfig, RandomSearch, RandomSearchConfig, Rvea, RveaConfig,
SeparableNes, SeparableNesConfig, SimulatedAnnealing,
SimulatedAnnealingConfig, SmsEmoa, SmsEmoaConfig, Spea2, Spea2Config, TabuSearch, SimulatedAnnealingConfig, SmsEmoa, SmsEmoaConfig, Spea2, Spea2Config, TabuSearch,
TabuSearchConfig, Tlbo, TlboConfig, Tpe, TpeConfig, Umda, TabuSearchConfig, Tlbo, TlboConfig, Tpe, TpeConfig, Umda, UmdaConfig,
UmdaConfig,
}; };
+1 -5
View File
@@ -9,11 +9,7 @@ use crate::core::rng::Rng;
/// ///
/// Returns cloned decisions. Panics if `population` is empty and `count > 0` /// Returns cloned decisions. Panics if `population` is empty and `count > 0`
/// (spec §10.1). /// (spec §10.1).
pub fn select_random<D: Clone>( pub fn select_random<D: Clone>(population: &[Candidate<D>], count: usize, rng: &mut Rng) -> Vec<D> {
population: &[Candidate<D>],
count: usize,
rng: &mut Rng,
) -> Vec<D> {
if count == 0 { if count == 0 {
return Vec::new(); return Vec::new();
} }
+15 -8
View File
@@ -63,8 +63,18 @@ fn challenger_wins<D>(c: &Candidate<D>, b: &Candidate<D>, dir: Direction) -> boo
(false, true) => false, (false, true) => false,
(false, false) => c.evaluation.constraint_violation < b.evaluation.constraint_violation, (false, false) => c.evaluation.constraint_violation < b.evaluation.constraint_violation,
(true, true) => { (true, true) => {
let cv = c.evaluation.objectives.first().copied().unwrap_or(f64::INFINITY); let cv = c
let bv = b.evaluation.objectives.first().copied().unwrap_or(f64::INFINITY); .evaluation
.objectives
.first()
.copied()
.unwrap_or(f64::INFINITY);
let bv = b
.evaluation
.objectives
.first()
.copied()
.unwrap_or(f64::INFINITY);
match dir { match dir {
Direction::Minimize => cv < bv, Direction::Minimize => cv < bv,
Direction::Maximize => cv > bv, Direction::Maximize => cv > bv,
@@ -218,10 +228,7 @@ mod tests {
#[test] #[test]
#[should_panic(expected = "exactly one objective")] #[should_panic(expected = "exactly one objective")]
fn multi_objective_panics() { fn multi_objective_panics() {
let s = ObjectiveSpace::new(vec![ let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
Objective::minimize("f1"),
Objective::minimize("f2"),
]);
let pop = [cand_min(1, 1.0)]; let pop = [cand_min(1, 1.0)];
let mut rng = rng_from_seed(0); let mut rng = rng_from_seed(0);
let _ = tournament_select_single_objective(&pop, &s, 2, 1, &mut rng); let _ = tournament_select_single_objective(&pop, &s, 2, 1, &mut rng);
@@ -246,8 +253,8 @@ mod tests {
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]); let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
let pop = [ let pop = [
Candidate::new(1u32, Evaluation::constrained(vec![0.0], 5.0)), // infeasible Candidate::new(1u32, Evaluation::constrained(vec![0.0], 5.0)), // infeasible
Candidate::new(2u32, Evaluation::new(vec![10.0])), // feasible, big f Candidate::new(2u32, Evaluation::new(vec![10.0])), // feasible, big f
Candidate::new(3u32, Evaluation::new(vec![3.0])), // feasible, small f Candidate::new(3u32, Evaluation::new(vec![3.0])), // feasible, small f
]; ];
let mut rng = rng_from_seed(0); let mut rng = rng_from_seed(0);
let picks = stochastic_ranking_select(&pop, &s, 0.0, 3, &mut rng); let picks = stochastic_ranking_select(&pop, &s, 0.0, 3, &mut rng);
+2 -2
View File
@@ -43,6 +43,7 @@ impl Problem for SchafferN1 {
} }
struct OneMax { struct OneMax {
#[allow(dead_code)]
bits: usize, bits: usize,
} }
impl Problem for OneMax { impl Problem for OneMax {
@@ -69,8 +70,7 @@ fn mo_bounds() -> Vec<(f64, f64)> {
vec![(-3.0, 3.0)] vec![(-3.0, 3.0)]
} }
fn mo_variation() fn mo_variation() -> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
-> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
let bounds = mo_bounds(); let bounds = mo_bounds();
CompositeVariation { CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
+6 -2
View File
@@ -9,8 +9,12 @@ use heuropt::prelude::*;
/// Generate per-axis bounds whose width is at least 0.001 (avoid the /// Generate per-axis bounds whose width is at least 0.001 (avoid the
/// degenerate `lo == hi` case for properties that need a proper interval). /// degenerate `lo == hi` case for properties that need a proper interval).
fn bounds(dim: usize) -> impl Strategy<Value = Vec<(f64, f64)>> { fn bounds(dim: usize) -> impl Strategy<Value = Vec<(f64, f64)>> {
prop::collection::vec((-50.0_f64..50.0, 0.001_f64..50.0), dim..=dim) prop::collection::vec((-50.0_f64..50.0, 0.001_f64..50.0), dim..=dim).prop_map(|pairs| {
.prop_map(|pairs| pairs.into_iter().map(|(lo, span)| (lo, lo + span)).collect()) pairs
.into_iter()
.map(|(lo, span)| (lo, lo + span))
.collect()
})
} }
/// Generate a parent vector inside the given bounds. /// Generate a parent vector inside the given bounds.
+8 -9
View File
@@ -20,17 +20,12 @@ use heuropt::prelude::*;
/// Generate a 2-objective minimize ObjectiveSpace. /// Generate a 2-objective minimize ObjectiveSpace.
fn space_2d() -> ObjectiveSpace { fn space_2d() -> ObjectiveSpace {
ObjectiveSpace::new(vec![ ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
Objective::minimize("f1"),
Objective::minimize("f2"),
])
} }
/// Generate a candidate with a 2-D objective vector in `[lo, hi]`. /// Generate a candidate with a 2-D objective vector in `[lo, hi]`.
fn candidate_2d(lo: f64, hi: f64) -> impl Strategy<Value = Candidate<()>> { fn candidate_2d(lo: f64, hi: f64) -> impl Strategy<Value = Candidate<()>> {
(lo..hi, lo..hi).prop_map(|(a, b)| { (lo..hi, lo..hi).prop_map(|(a, b)| Candidate::new((), Evaluation::new(vec![a, b])))
Candidate::new((), Evaluation::new(vec![a, b]))
})
} }
/// Generate a small 2-D population. /// Generate a small 2-D population.
@@ -40,8 +35,12 @@ fn population_2d() -> impl Strategy<Value = Vec<Candidate<()>>> {
/// Generate per-axis bounds. /// Generate per-axis bounds.
fn bounds(dim: usize) -> impl Strategy<Value = Vec<(f64, f64)>> { fn bounds(dim: usize) -> impl Strategy<Value = Vec<(f64, f64)>> {
prop::collection::vec((-50.0_f64..50.0, 0.001_f64..50.0), dim..=dim) prop::collection::vec((-50.0_f64..50.0, 0.001_f64..50.0), dim..=dim).prop_map(|pairs| {
.prop_map(|pairs| pairs.into_iter().map(|(lo, span)| (lo, lo + span)).collect()) pairs
.into_iter()
.map(|(lo, span)| (lo, lo + span))
.collect()
})
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------