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