Files
heuropt/tests/algorithm_properties.rs
T
swaits 2ca8d71b94 test(snapshot): pin exact run() output for every algorithm
The full-codebase mutants run showed ~600 of the 902 surviving mutants
are arithmetic / comparison flips inside algorithm run() bodies — the
per-helper Phase 1 tests don't reach the optimization loop itself, and
the existing deterministic-with-same-seed tests can't catch them (both
the clean and mutated runs use the same seed, so they still match).

This extends the async-parity sweep: each of the 33 parity tests now
also asserts the sync run's result against an exact captured snapshot
(best objectives for single-objective algorithms; sorted pareto-front
objective tuples for multi-objective ones). Any arithmetic flip
anywhere in run() perturbs at least one f64 and breaks the snapshot.

Fixtures are deliberately multi-dimensional — SnapSphere (3-D sum of
squares), SnapMo (3-variable / 2-objective), a 6-city TinyTsp, a 3-D
SnapSpherePartial for Hyperband. A 1-D problem leaves the per-axis /
covariance-matrix / simplex machinery degenerate, so arithmetic
mutations there wouldn't change the result; 3-D exercises the full
loop body.

Snapshots captured from the un-mutated implementation; an intentional
algorithm change requires regenerating them, by design. The assertions
live in the async-gated module because they reuse its per-algorithm
constructions — active during the mutation campaign
(--features async,serde) and under cargo test --features async.
2026-05-14 03:41:38 -06:00

2584 lines
84 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Per-algorithm property tests.
//!
//! For every `Optimizer` impl in heuropt we check the same three properties:
//! 1. **Deterministic-with-seed**: two runs with the same seed produce
//! the same `best.evaluation.objectives`.
//! 2. **No panic on random valid inputs**: random seeds, random tiny
//! problems, random bounds — the algorithm runs to completion.
//! 3. **Population-size invariant** (where the algorithm documents one):
//! the final population has the configured size.
use proptest::prelude::*;
use heuropt::core::evaluation::Evaluation;
use heuropt::core::objective::{Objective, ObjectiveSpace};
use heuropt::core::problem::Problem;
use heuropt::prelude::*;
// -----------------------------------------------------------------------------
// Tiny problems
// -----------------------------------------------------------------------------
struct Sphere1D;
impl Problem for Sphere1D {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
Evaluation::new(vec![x[0] * x[0]])
}
}
struct SchafferN1;
impl Problem for SchafferN1 {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let v = x[0];
Evaluation::new(vec![v * v, (v - 2.0).powi(2)])
}
}
struct OneMax {
#[allow(dead_code)]
bits: usize,
}
impl Problem for OneMax {
type Decision = Vec<bool>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::maximize("count")])
}
fn evaluate(&self, x: &Vec<bool>) -> Evaluation {
Evaluation::new(vec![x.iter().filter(|b| **b).count() as f64])
}
}
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
fn so_bounds() -> RealBounds {
RealBounds::new(vec![(-3.0, 3.0)])
}
fn so_bounds_2d() -> RealBounds {
RealBounds::new(vec![(-3.0, 3.0); 2])
}
fn mo_bounds() -> Vec<(f64, f64)> {
vec![(-3.0, 3.0)]
}
fn mo_variation() -> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
let bounds = mo_bounds();
CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}
}
// -----------------------------------------------------------------------------
// Single-objective continuous
// -----------------------------------------------------------------------------
proptest! {
#[test]
fn random_search_deterministic(seed in any::<u64>()) {
let make = || RandomSearch::new(
RandomSearchConfig { iterations: 20, batch_size: 1, seed },
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn hill_climber_deterministic(seed in any::<u64>()) {
let make = || HillClimber::new(
HillClimberConfig { iterations: 20, seed },
so_bounds(),
GaussianMutation { sigma: 0.1 },
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn one_plus_one_es_deterministic(seed in any::<u64>()) {
let make = || OnePlusOneEs::new(
OnePlusOneEsConfig {
iterations: 50,
initial_sigma: 0.5,
adaptation_period: 10,
step_increase: 1.22,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn simulated_annealing_deterministic(seed in any::<u64>()) {
let make = || SimulatedAnnealing::new(
SimulatedAnnealingConfig {
iterations: 50,
initial_temperature: 1.0,
final_temperature: 1e-3,
seed,
},
so_bounds(),
GaussianMutation { sigma: 0.1 },
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn ga_deterministic(seed in any::<u64>()) {
let bounds = mo_bounds();
let make = || GeneticAlgorithm::new(
GeneticAlgorithmConfig {
population_size: 10,
generations: 5,
tournament_size: 2,
elitism: 1,
seed,
},
RealBounds::new(bounds.clone()),
CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds.clone(), 20.0, 1.0),
},
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn pso_deterministic(seed in any::<u64>()) {
let make = || ParticleSwarm::new(
ParticleSwarmConfig {
swarm_size: 10,
generations: 5,
inertia: 0.7,
cognitive: 1.5,
social: 1.5,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn de_deterministic(seed in any::<u64>()) {
let make = || DifferentialEvolution::new(
DifferentialEvolutionConfig {
population_size: 10,
generations: 5,
differential_weight: 0.5,
crossover_probability: 0.9,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn cmaes_deterministic(seed in any::<u64>()) {
let cfg = CmaEsConfig {
population_size: 8,
generations: 5,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
initial_mean: None,
seed,
};
let mut a = CmaEs::new(cfg.clone(), so_bounds());
let mut b = CmaEs::new(cfg, so_bounds());
let r1 = a.run(&Sphere1D);
let r2 = b.run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn ipop_cmaes_deterministic(seed in any::<u64>()) {
let cfg = IpopCmaEsConfig {
initial_population_size: 8,
total_generations: 30,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
stall_generations: None,
seed,
};
let mut a = IpopCmaEs::new(cfg.clone(), so_bounds());
let mut b = IpopCmaEs::new(cfg, so_bounds());
let r1 = a.run(&Sphere1D);
let r2 = b.run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn snes_deterministic(seed in any::<u64>()) {
let make = || SeparableNes::new(
SeparableNesConfig {
population_size: 8,
generations: 5,
initial_sigma: 0.5,
mean_learning_rate: 1.0,
sigma_learning_rate: None,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn tlbo_deterministic(seed in any::<u64>()) {
let make = || Tlbo::new(
TlboConfig { population_size: 10, generations: 5, seed },
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn nelder_mead_deterministic(_dummy in any::<bool>()) {
// Nelder-Mead is purely deterministic; no seed.
let make = || NelderMead::new(
NelderMeadConfig { iterations: 50, ..NelderMeadConfig::default() },
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn bayesian_opt_deterministic(seed in any::<u64>()) {
let make = || BayesianOpt::new(
BayesianOptConfig {
initial_samples: 5,
iterations: 10,
length_scales: None,
signal_variance: 1.0,
noise_variance: 1e-6,
acquisition_samples: 100,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn tpe_deterministic(seed in any::<u64>()) {
let make = || Tpe::new(
TpeConfig {
initial_samples: 5,
iterations: 10,
good_fraction: 0.25,
candidate_samples: 12,
bandwidth_factor: 1.0,
seed,
},
so_bounds(),
);
let r1 = make().run(&Sphere1D);
let r2 = make().run(&Sphere1D);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
}
// -----------------------------------------------------------------------------
// Multi-objective
// -----------------------------------------------------------------------------
proptest! {
#[test]
fn nsga2_deterministic_and_pop_size(seed in any::<u64>()) {
let make = || Nsga2::new(
Nsga2Config { population_size: 10, generations: 3, seed },
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
prop_assert_eq!(r1.population.len(), 10);
}
#[test]
fn nsga3_deterministic_and_pop_size(seed in any::<u64>()) {
let make = || Nsga3::new(
Nsga3Config {
population_size: 12,
generations: 3,
reference_divisions: 11,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
prop_assert_eq!(r1.population.len(), 12);
}
#[test]
fn spea2_deterministic(seed in any::<u64>()) {
let make = || Spea2::new(
Spea2Config {
population_size: 10,
archive_size: 10,
generations: 3,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn moead_deterministic(seed in any::<u64>()) {
let make = || Moead::new(
MoeadConfig {
generations: 3,
reference_divisions: 9,
neighborhood_size: 4,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.population.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.population.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn mopso_deterministic(seed in any::<u64>()) {
let make = || Mopso::new(
MopsoConfig {
swarm_size: 10,
generations: 3,
archive_size: 10,
inertia: 0.7,
cognitive: 1.5,
social: 1.5,
seed,
},
RealBounds::new(mo_bounds()),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn ibea_deterministic(seed in any::<u64>()) {
let make = || Ibea::new(
IbeaConfig {
population_size: 10,
generations: 3,
kappa: 0.05,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn sms_emoa_deterministic(seed in any::<u64>()) {
let make = || SmsEmoa::new(
SmsEmoaConfig {
population_size: 8,
generations: 5,
reference_point: vec![10.0, 10.0],
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn hype_deterministic(seed in any::<u64>()) {
let make = || Hype::new(
HypeConfig {
population_size: 10,
generations: 3,
reference_point: vec![10.0, 10.0],
mc_samples: 100,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn pesa2_deterministic(seed in any::<u64>()) {
let make = || PesaII::new(
PesaIIConfig {
population_size: 10,
archive_size: 10,
generations: 3,
grid_divisions: 4,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn epsilon_moea_deterministic(seed in any::<u64>()) {
let make = || EpsilonMoea::new(
EpsilonMoeaConfig {
population_size: 10,
evaluations: 30,
epsilon: vec![0.05, 0.05],
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn age_moea_deterministic(seed in any::<u64>()) {
let make = || AgeMoea::new(
AgeMoeaConfig { population_size: 10, generations: 3, seed },
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn grea_deterministic(seed in any::<u64>()) {
let make = || Grea::new(
GreaConfig {
population_size: 10,
generations: 3,
grid_divisions: 4,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn knea_deterministic(seed in any::<u64>()) {
let make = || Knea::new(
KneaConfig { population_size: 10, generations: 3, seed },
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn rvea_deterministic(seed in any::<u64>()) {
let make = || Rvea::new(
RveaConfig {
population_size: 10,
generations: 3,
reference_divisions: 9,
alpha: 2.0,
seed,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
#[test]
fn paes_deterministic(seed in any::<u64>()) {
let make = || Paes::new(
PaesConfig { iterations: 30, archive_size: 10, seed },
RealBounds::new(mo_bounds()),
GaussianMutation { sigma: 0.1 },
);
let r1 = make().run(&SchafferN1);
let r2 = make().run(&SchafferN1);
let oa: Vec<Vec<f64>> = r1.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
let ob: Vec<Vec<f64>> = r2.pareto_front.iter()
.map(|c| c.evaluation.objectives.clone()).collect();
prop_assert_eq!(oa, ob);
}
}
// -----------------------------------------------------------------------------
// Other decision types
// -----------------------------------------------------------------------------
proptest! {
#[test]
fn umda_deterministic(seed in any::<u64>(), bits in 4usize..16) {
let problem = OneMax { bits };
let make = || Umda::new(UmdaConfig {
population_size: 10,
selected_size: 5,
generations: 3,
bits,
seed,
});
let r1 = make().run(&problem);
let r2 = make().run(&problem);
prop_assert_eq!(
r1.best.unwrap().evaluation.objectives,
r2.best.unwrap().evaluation.objectives,
);
}
#[test]
fn random_search_evaluation_count_invariant(
iterations in 1usize..30,
batch_size in 1usize..5,
seed in any::<u64>(),
) {
let mut opt = RandomSearch::new(
RandomSearchConfig { iterations, batch_size, seed },
so_bounds(),
);
let r = opt.run(&Sphere1D);
prop_assert_eq!(r.evaluations, iterations * batch_size);
prop_assert_eq!(r.population.len(), iterations * batch_size);
prop_assert_eq!(r.generations, iterations);
}
}
// -----------------------------------------------------------------------------
// Cross-cutting: best is at least as good as any front member
// -----------------------------------------------------------------------------
proptest! {
#[test]
fn so_optimizer_best_beats_initial(seed in any::<u64>()) {
// After running an SO optimizer, the result's best.evaluation
// should be at least as good as the worst point sampled — i.e.
// the optimizer doesn't return None or some random non-best.
let mut opt = DifferentialEvolution::new(
DifferentialEvolutionConfig {
population_size: 10,
generations: 5,
differential_weight: 0.5,
crossover_probability: 0.9,
seed,
},
so_bounds_2d(),
);
let r = opt.run(&Sphere1D);
let best_f = r.best.unwrap().evaluation.objectives[0];
let pop_min = r.population.iter()
.map(|c| c.evaluation.objectives[0])
.fold(f64::INFINITY, f64::min);
prop_assert!(
best_f <= pop_min + 1e-12,
"best f = {best_f}, pop min = {pop_min}",
);
}
}
// -----------------------------------------------------------------------------
// AlgorithmInfo sweep — exact name / full_name / seed per algorithm
// -----------------------------------------------------------------------------
//
// Why this exists: every algorithm has three trivial trait methods returning
// `&'static str` and `Option<u64>`. A `cargo mutants` run discovers that
// these are unconstrained — replacing `"NSGA-II"` with `""` or `"xyzzy"`
// survives because no test reads the string. The constants below pin every
// algorithm's identifying strings exactly. Updating an algorithm's name
// requires updating its test, by design.
#[test]
fn age_moea_algorithm_info_is_correct() {
let opt = AgeMoea::new(
AgeMoeaConfig {
population_size: 4,
generations: 1,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "AGE-MOEA");
assert_eq!(
opt.full_name(),
"Adaptive Geometry Estimation Multi-Objective Evolutionary Algorithm",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn ant_colony_tsp_algorithm_info_is_correct() {
let opt = AntColonyTsp::new(
AntColonyTspConfig {
ants: 2,
generations: 1,
alpha: 1.0,
beta: 2.0,
evaporation: 0.5,
deposit: 1.0,
initial_pheromone: 1.0,
seed: 42,
},
vec![vec![0.0, 1.0], vec![1.0, 0.0]],
);
assert_eq!(opt.name(), "Ant Colony");
assert_eq!(opt.full_name(), "Ant Colony System for TSP");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn bayesian_opt_algorithm_info_is_correct() {
let opt = BayesianOpt::new(
BayesianOptConfig {
initial_samples: 2,
iterations: 1,
length_scales: None,
signal_variance: 1.0,
noise_variance: 1e-3,
acquisition_samples: 4,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "Bayesian Optimization");
assert_eq!(
opt.full_name(),
"Gaussian Process Bayesian Optimization with Expected Improvement",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn cma_es_algorithm_info_is_correct() {
let opt = CmaEs::new(
CmaEsConfig {
population_size: 4,
generations: 1,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
initial_mean: None,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "CMA-ES");
assert_eq!(
opt.full_name(),
"Covariance Matrix Adaptation Evolution Strategy"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn differential_evolution_algorithm_info_is_correct() {
let opt = DifferentialEvolution::new(
DifferentialEvolutionConfig {
population_size: 4,
generations: 1,
differential_weight: 0.5,
crossover_probability: 0.9,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "DE");
assert_eq!(opt.full_name(), "Differential Evolution");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn epsilon_moea_algorithm_info_is_correct() {
let opt = EpsilonMoea::new(
EpsilonMoeaConfig {
population_size: 4,
evaluations: 4,
epsilon: vec![0.1, 0.1],
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "ε-MOEA");
assert_eq!(
opt.full_name(),
"ε-dominance Multi-Objective Evolutionary Algorithm",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn genetic_algorithm_algorithm_info_is_correct() {
let bounds = mo_bounds();
let opt = GeneticAlgorithm::new(
GeneticAlgorithmConfig {
population_size: 4,
generations: 1,
tournament_size: 2,
elitism: 1,
seed: 42,
},
RealBounds::new(bounds.clone()),
CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
},
);
assert_eq!(opt.name(), "GA");
assert_eq!(opt.full_name(), "Genetic Algorithm");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn grea_algorithm_info_is_correct() {
let opt = Grea::new(
GreaConfig {
population_size: 4,
generations: 1,
grid_divisions: 4,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "GrEA");
assert_eq!(opt.full_name(), "Grid-based Evolutionary Algorithm");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn hill_climber_algorithm_info_is_correct() {
let opt = HillClimber::new(
HillClimberConfig {
iterations: 1,
seed: 42,
},
so_bounds(),
GaussianMutation { sigma: 0.1 },
);
assert_eq!(opt.name(), "Hill Climber");
assert_eq!(opt.full_name(), "Hill Climbing");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn hyperband_algorithm_info_is_correct() {
let opt: Hyperband<RealBounds, Vec<f64>> = Hyperband::new(
HyperbandConfig {
max_budget: 8.0,
eta: 2.0,
max_brackets: 2,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "Hyperband");
assert_eq!(opt.full_name(), "Hyperband multi-fidelity bandit search");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn hype_algorithm_info_is_correct() {
let opt = Hype::new(
HypeConfig {
population_size: 4,
generations: 1,
reference_point: vec![10.0, 10.0],
mc_samples: 4,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "HypE");
assert_eq!(opt.full_name(), "Hypervolume Estimation Algorithm");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn ibea_algorithm_info_is_correct() {
let opt = Ibea::new(
IbeaConfig {
population_size: 4,
generations: 1,
kappa: 0.05,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "IBEA");
assert_eq!(opt.full_name(), "Indicator-Based Evolutionary Algorithm");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn ipop_cma_es_algorithm_info_is_correct() {
let opt = IpopCmaEs::new(
IpopCmaEsConfig {
initial_population_size: 4,
total_generations: 1,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
stall_generations: None,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "IPOP-CMA-ES");
assert_eq!(
opt.full_name(),
"Increasing-Population CMA-ES with Restarts"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn knea_algorithm_info_is_correct() {
let opt = Knea::new(
KneaConfig {
population_size: 4,
generations: 1,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "KnEA");
assert_eq!(opt.full_name(), "Knee point-driven Evolutionary Algorithm");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn moead_algorithm_info_is_correct() {
let opt = Moead::new(
MoeadConfig {
generations: 1,
reference_divisions: 3,
neighborhood_size: 2,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "MOEA/D");
assert_eq!(
opt.full_name(),
"Multi-Objective Evolutionary Algorithm based on Decomposition",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn mopso_algorithm_info_is_correct() {
let opt = Mopso::new(
MopsoConfig {
swarm_size: 4,
generations: 1,
archive_size: 4,
inertia: 0.5,
cognitive: 1.0,
social: 1.0,
seed: 42,
},
RealBounds::new(mo_bounds()),
);
assert_eq!(opt.name(), "MOPSO");
assert_eq!(
opt.full_name(),
"Multi-Objective Particle Swarm Optimization"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn nelder_mead_algorithm_info_is_correct() {
let opt = NelderMead::new(
NelderMeadConfig {
iterations: 1,
..NelderMeadConfig::default()
},
so_bounds(),
);
assert_eq!(opt.name(), "Nelder-Mead");
assert_eq!(opt.full_name(), "Nelder-Mead simplex direct search");
// NelderMead is deterministic — no seed. Matches the default AlgorithmInfo
// impl which returns None.
assert_eq!(opt.seed(), None);
}
#[test]
fn nsga2_algorithm_info_is_correct() {
let opt = Nsga2::new(
Nsga2Config {
population_size: 4,
generations: 1,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "NSGA-II");
assert_eq!(
opt.full_name(),
"Non-dominated Sorting Genetic Algorithm II"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn nsga3_algorithm_info_is_correct() {
let opt = Nsga3::new(
Nsga3Config {
population_size: 4,
generations: 1,
reference_divisions: 4,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "NSGA-III");
assert_eq!(
opt.full_name(),
"Non-dominated Sorting Genetic Algorithm III"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn one_plus_one_es_algorithm_info_is_correct() {
let opt = OnePlusOneEs::new(
OnePlusOneEsConfig {
iterations: 1,
initial_sigma: 0.5,
adaptation_period: 4,
step_increase: 1.5,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "(1+1)-ES");
assert_eq!(
opt.full_name(),
"(1+1) Evolution Strategy with one-fifth success rule",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn paes_algorithm_info_is_correct() {
let opt = Paes::new(
PaesConfig {
iterations: 1,
archive_size: 4,
seed: 42,
},
RealBounds::new(mo_bounds()),
GaussianMutation { sigma: 0.1 },
);
assert_eq!(opt.name(), "PAES");
assert_eq!(opt.full_name(), "Pareto Archived Evolution Strategy");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn particle_swarm_algorithm_info_is_correct() {
let opt = ParticleSwarm::new(
ParticleSwarmConfig {
swarm_size: 4,
generations: 1,
inertia: 0.5,
cognitive: 1.0,
social: 1.0,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "PSO");
assert_eq!(opt.full_name(), "Particle Swarm Optimization");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn pesa_ii_algorithm_info_is_correct() {
let opt = PesaII::new(
PesaIIConfig {
population_size: 4,
archive_size: 4,
generations: 1,
grid_divisions: 4,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "PESA-II");
assert_eq!(
opt.full_name(),
"Pareto Envelope-based Selection Algorithm II"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn random_search_algorithm_info_is_correct() {
let opt = RandomSearch::new(
RandomSearchConfig {
iterations: 1,
batch_size: 1,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "Random Search");
// No `full_name` override — defaults to `name`.
assert_eq!(opt.full_name(), "Random Search");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn rvea_algorithm_info_is_correct() {
let opt = Rvea::new(
RveaConfig {
population_size: 4,
generations: 1,
reference_divisions: 4,
alpha: 2.0,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "RVEA");
assert_eq!(
opt.full_name(),
"Reference Vector-guided Evolutionary Algorithm"
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn simulated_annealing_algorithm_info_is_correct() {
let opt = SimulatedAnnealing::new(
SimulatedAnnealingConfig {
iterations: 1,
initial_temperature: 1.0,
final_temperature: 0.1,
seed: 42,
},
so_bounds(),
GaussianMutation { sigma: 0.1 },
);
assert_eq!(opt.name(), "Simulated Annealing");
// No `full_name` override — defaults to `name`.
assert_eq!(opt.full_name(), "Simulated Annealing");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn sms_emoa_algorithm_info_is_correct() {
let opt = SmsEmoa::new(
SmsEmoaConfig {
population_size: 4,
generations: 1,
reference_point: vec![100.0, 100.0],
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "SMS-EMOA");
assert_eq!(
opt.full_name(),
"S-Metric Selection Evolutionary Multi-Objective Algorithm",
);
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn separable_nes_algorithm_info_is_correct() {
let opt = SeparableNes::new(
SeparableNesConfig {
population_size: 4,
generations: 1,
initial_sigma: 0.5,
mean_learning_rate: 1.0,
sigma_learning_rate: Some(0.1),
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "sNES");
assert_eq!(opt.full_name(), "Separable Natural Evolution Strategy");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn spea2_algorithm_info_is_correct() {
let opt = Spea2::new(
Spea2Config {
population_size: 4,
archive_size: 4,
generations: 1,
seed: 42,
},
RealBounds::new(mo_bounds()),
mo_variation(),
);
assert_eq!(opt.name(), "SPEA2");
assert_eq!(opt.full_name(), "Strength Pareto Evolutionary Algorithm 2");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn tabu_search_algorithm_info_is_correct() {
struct StartAtZero;
impl Initializer<Vec<i32>> for StartAtZero {
fn initialize(
&mut self,
_size: usize,
_rng: &mut heuropt::core::rng::Rng,
) -> Vec<Vec<i32>> {
vec![vec![0]]
}
}
let neighbors =
|x: &Vec<i32>, _rng: &mut heuropt::core::rng::Rng| vec![vec![x[0] - 1], vec![x[0] + 1]];
let opt = TabuSearch::new(
TabuSearchConfig {
iterations: 1,
tabu_tenure: 4,
seed: 42,
},
StartAtZero,
neighbors,
);
assert_eq!(opt.name(), "Tabu Search");
// No `full_name` override — defaults to `name`.
assert_eq!(opt.full_name(), "Tabu Search");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn tlbo_algorithm_info_is_correct() {
let opt = Tlbo::new(
TlboConfig {
population_size: 4,
generations: 1,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "TLBO");
assert_eq!(opt.full_name(), "Teaching-Learning-Based Optimization");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn tpe_algorithm_info_is_correct() {
let opt = Tpe::new(
TpeConfig {
initial_samples: 2,
iterations: 1,
good_fraction: 0.25,
candidate_samples: 4,
bandwidth_factor: 0.1,
seed: 42,
},
so_bounds(),
);
assert_eq!(opt.name(), "TPE");
assert_eq!(opt.full_name(), "Tree-structured Parzen Estimator");
assert_eq!(opt.seed(), Some(42));
}
#[test]
fn umda_algorithm_info_is_correct() {
let opt = Umda::new(UmdaConfig {
bits: 4,
population_size: 4,
selected_size: 2,
generations: 1,
seed: 42,
});
assert_eq!(opt.name(), "UMDA");
assert_eq!(
opt.full_name(),
"Univariate Marginal Distribution Algorithm"
);
assert_eq!(opt.seed(), Some(42));
}
// -----------------------------------------------------------------------------
// run_async ↔ run parity sweep
// -----------------------------------------------------------------------------
//
// Every algorithm exposes both `run` and `run_async` (the latter behind the
// `async` feature). Before this sweep, nothing exercised `run_async`, so a
// `cargo mutants` run survived essentially every mutation to its body —
// "replace run_async with OptimizationResult::new()", every comparison flip,
// every += → -= inside the async loop. This sweep asserts that with the
// same Config + seed + problem, the async runner produces *identical*
// best.evaluation.objectives as the sync runner. The two implementations
// share the algorithmic logic; only the evaluation dispatch differs.
//
// Hyperband uses AsyncPartialProblem (multi-fidelity) instead of
// AsyncProblem, so it gets its own test fixture below.
#[cfg(feature = "async")]
mod async_parity {
use super::*;
use heuropt::core::async_problem::{AsyncPartialProblem, AsyncProblem};
use heuropt::core::partial_problem::PartialProblem;
// ---- Async-capable test fixtures ----------------------------------------
//
// These are multi-dimensional on purpose: the snapshot assertions below
// pin each algorithm's exact run() output, and a 1-D problem leaves the
// per-axis / covariance-matrix / simplex machinery degenerate, so
// arithmetic mutations there wouldn't change the result. 3-D problems
// exercise the full loop body.
/// 3-D Rosenbrock: f(x) = Σ [100·(x_{i+1} x_i²)² + (1 x_i)²].
/// Single objective. Deliberately *hard*: a curved, non-convex valley
/// that none of these algorithms fully solve in a modest budget. That
/// matters for the snapshot assertions — on a convex sphere the
/// optimizers converge to the exact optimum regardless of small
/// arithmetic perturbations, so a mutated run() still lands on 0.0 and
/// the snapshot can't tell the difference. On Rosenbrock the result
/// always reflects the exact trajectory.
struct SnapSphere;
impl Problem for SnapSphere {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let mut f = 0.0;
for i in 0..x.len() - 1 {
let a = x[i + 1] - x[i] * x[i];
let b = 1.0 - x[i];
f += 100.0 * a * a + b * b;
}
Evaluation::new(vec![f])
}
}
impl AsyncProblem for SnapSphere {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
<Self as Problem>::objectives(self)
}
async fn evaluate_async(&self, x: &Vec<f64>) -> Evaluation {
<Self as Problem>::evaluate(self, x)
}
}
/// 3-variable, 2-objective problem: f1 = Σ xᵢ², f2 = Σ (xᵢ 2)².
/// A genuine multi-variable Pareto front, unlike the 1-variable
/// SchafferN1 used elsewhere in this file.
struct SnapMo;
impl Problem for SnapMo {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn evaluate(&self, x: &Vec<f64>) -> Evaluation {
let f1: f64 = x.iter().map(|v| v * v).sum();
let f2: f64 = x.iter().map(|v| (v - 2.0) * (v - 2.0)).sum();
Evaluation::new(vec![f1, f2])
}
}
impl AsyncProblem for SnapMo {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
<Self as Problem>::objectives(self)
}
async fn evaluate_async(&self, x: &Vec<f64>) -> Evaluation {
<Self as Problem>::evaluate(self, x)
}
}
impl AsyncProblem for OneMax {
type Decision = Vec<bool>;
fn objectives(&self) -> ObjectiveSpace {
<Self as Problem>::objectives(self)
}
async fn evaluate_async(&self, x: &Vec<bool>) -> Evaluation {
<Self as Problem>::evaluate(self, x)
}
}
fn snap_so_bounds() -> RealBounds {
RealBounds::new(vec![(-3.0, 3.0); 3])
}
fn snap_mo_bounds() -> Vec<(f64, f64)> {
vec![(-3.0, 3.0); 3]
}
fn snap_mo_variation() -> CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation> {
let bounds = snap_mo_bounds();
CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
}
}
/// 8-city scattered 2-D TSP fixture for AntColonyTsp parity. The cities
/// are deliberately *not* on a line: a nearest-neighbour / distance-
/// heuristic-only tour is **not** optimal here, so the pheromone-update
/// arithmetic in `run()` genuinely determines which tour is found.
/// (A collinear instance is solved by the heuristic alone, which makes
/// the pheromone math inert and its mutations undetectable.)
struct TinyTsp {
dist: Vec<Vec<f64>>,
}
impl TinyTsp {
fn new() -> Self {
// 8 scattered cities — irregular 2-D layout with several
// near-equal competing edges so pheromone reinforcement is
// load-bearing for the recovered tour.
let pts = [
(0.0_f64, 0.0_f64),
(4.0, 1.0),
(1.0, 3.0),
(5.0, 4.0),
(2.0, 5.0),
(6.0, 2.0),
(3.0, 6.0),
(0.5, 4.5),
];
let n = pts.len();
let mut dist = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
let dx = pts[i].0 - pts[j].0;
let dy = pts[i].1 - pts[j].1;
dist[i][j] = (dx * dx + dy * dy).sqrt();
}
}
Self { dist }
}
fn length(&self, tour: &[usize]) -> f64 {
let n = tour.len();
let mut total = 0.0;
for i in 0..n {
total += self.dist[tour[i]][tour[(i + 1) % n]];
}
total
}
}
impl Problem for TinyTsp {
type Decision = Vec<usize>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("length")])
}
fn evaluate(&self, t: &Vec<usize>) -> Evaluation {
Evaluation::new(vec![self.length(t)])
}
}
impl AsyncProblem for TinyTsp {
type Decision = Vec<usize>;
fn objectives(&self) -> ObjectiveSpace {
<Self as Problem>::objectives(self)
}
async fn evaluate_async(&self, t: &Vec<usize>) -> Evaluation {
<Self as Problem>::evaluate(self, t)
}
}
/// Trivial integer problem for TabuSearch — minimize |x|.
struct AbsInt;
impl Problem for AbsInt {
type Decision = Vec<i32>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("absx")])
}
fn evaluate(&self, x: &Vec<i32>) -> Evaluation {
Evaluation::new(vec![x[0].unsigned_abs() as f64])
}
}
impl AsyncProblem for AbsInt {
type Decision = Vec<i32>;
fn objectives(&self) -> ObjectiveSpace {
<Self as Problem>::objectives(self)
}
async fn evaluate_async(&self, x: &Vec<i32>) -> Evaluation {
<Self as Problem>::evaluate(self, x)
}
}
/// 3-D multi-fidelity wrapper for Hyperband — the underlying objective
/// is 3-D Rosenbrock, but evaluations are **budget-sensitive**: a
/// low-budget evaluation is biased high by a deterministic per-x
/// penalty that decays as `1 / budget`. This matters for the snapshot
/// assertion: a budget-*insensitive* problem makes Hyperband's bracket
/// / rung / budget arithmetic inert (every config is judged the same
/// regardless of allocation), so mutations there can't be detected.
/// With a budget-sensitive problem the recovered best reflects exactly
/// which configs Hyperband promoted to which budgets.
struct SnapSpherePartial;
impl SnapSpherePartial {
fn rosenbrock(x: &[f64]) -> f64 {
let mut f = 0.0;
for i in 0..x.len() - 1 {
let a = x[i + 1] - x[i] * x[i];
let b = 1.0 - x[i];
f += 100.0 * a * a + b * b;
}
f
}
/// Deterministic per-x bias, scaled by 1/budget. Higher budget →
/// smaller bias → more accurate estimate (the PartialProblem
/// monotonicity contract).
fn budgeted(x: &[f64], budget: f64) -> f64 {
let bias: f64 = x.iter().map(|v| v.abs()).sum();
Self::rosenbrock(x) + 50.0 * bias / budget.max(1.0)
}
}
impl PartialProblem for SnapSpherePartial {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f")])
}
fn evaluate_at_budget(&self, x: &Vec<f64>, budget: f64) -> Evaluation {
Evaluation::new(vec![Self::budgeted(x, budget)])
}
}
impl AsyncPartialProblem for SnapSpherePartial {
type Decision = Vec<f64>;
fn objectives(&self) -> ObjectiveSpace {
<Self as PartialProblem>::objectives(self)
}
async fn evaluate_at_budget_async(&self, x: &Vec<f64>, budget: f64) -> Evaluation {
<Self as PartialProblem>::evaluate_at_budget(self, x, budget)
}
}
fn objectives_of<D>(r: &OptimizationResult<D>) -> Vec<f64> {
r.best
.as_ref()
.map(|c| c.evaluation.objectives.clone())
.unwrap_or_default()
}
// ---- Per-algorithm parity tests -----------------------------------------
#[tokio::test]
async fn random_search_async_matches_sync() {
let cfg = RandomSearchConfig {
iterations: 8,
batch_size: 1,
seed: 42,
};
let mut a = RandomSearch::new(cfg.clone(), snap_so_bounds());
let mut b = RandomSearch::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![63.306134606086815],
vec![704.5027621820259],
vec![2710.1445719354297],
vec![4282.489345404012],
vec![4362.422716839316],
vec![7901.9472922577515],
vec![8445.708398022229],
vec![10473.450683629166]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn hill_climber_async_matches_sync() {
let cfg = HillClimberConfig {
iterations: 40,
seed: 42,
};
let mut a = HillClimber::new(
cfg.clone(),
snap_so_bounds(),
GaussianMutation { sigma: 0.1 },
);
let mut b = HillClimber::new(cfg, snap_so_bounds(), GaussianMutation { sigma: 0.1 });
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![0.8713282461232607]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn one_plus_one_es_async_matches_sync() {
let cfg = OnePlusOneEsConfig {
iterations: 40,
initial_sigma: 0.5,
adaptation_period: 4,
step_increase: 1.5,
seed: 42,
};
let mut a = OnePlusOneEs::new(cfg.clone(), snap_so_bounds());
let mut b = OnePlusOneEs::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![1.448688485637438]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn simulated_annealing_async_matches_sync() {
let cfg = SimulatedAnnealingConfig {
iterations: 40,
initial_temperature: 1.0,
final_temperature: 0.1,
seed: 42,
};
let mut a = SimulatedAnnealing::new(
cfg.clone(),
snap_so_bounds(),
GaussianMutation { sigma: 0.1 },
);
let mut b = SimulatedAnnealing::new(cfg, snap_so_bounds(), GaussianMutation { sigma: 0.1 });
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![1.8936764528185597]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn genetic_algorithm_async_matches_sync() {
let bounds = vec![(-3.0_f64, 3.0); 3];
let cfg = GeneticAlgorithmConfig {
population_size: 6,
generations: 25,
tournament_size: 2,
elitism: 1,
seed: 42,
};
let make = || {
GeneticAlgorithm::new(
cfg.clone(),
RealBounds::new(bounds.clone()),
CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds.clone(), 20.0, 1.0),
},
)
};
let r_sync = make().run(&SnapSphere);
let r_async = make().run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![1.473670536153147],
vec![3.5514321351551894],
vec![10.006935556353785],
vec![13.100758990828941],
vec![84.24433280566845],
vec![102.49327038081938]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn particle_swarm_async_matches_sync() {
let cfg = ParticleSwarmConfig {
swarm_size: 6,
generations: 25,
inertia: 0.5,
cognitive: 1.0,
social: 1.0,
seed: 42,
};
let mut a = ParticleSwarm::new(cfg.clone(), snap_so_bounds());
let mut b = ParticleSwarm::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.0014273534965355294],
vec![0.0016092898775173475],
vec![0.0016360280965425223],
vec![0.0028941361029547383],
vec![0.0033691650194037533],
vec![0.3357310584047361]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn differential_evolution_async_matches_sync() {
let cfg = DifferentialEvolutionConfig {
population_size: 6,
generations: 25,
differential_weight: 0.5,
crossover_probability: 0.9,
seed: 42,
};
let mut a = DifferentialEvolution::new(cfg.clone(), snap_so_bounds());
let mut b = DifferentialEvolution::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![1.0209899358667007],
vec![1.177250863812577],
vec![1.1849640927940035],
vec![1.2565307288266827],
vec![1.2651999986995113],
vec![1.296215885697605]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn cma_es_async_matches_sync() {
let cfg = CmaEsConfig {
population_size: 6,
generations: 25,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
initial_mean: None,
seed: 42,
};
let mut a = CmaEs::new(cfg.clone(), snap_so_bounds());
let mut b = CmaEs::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![0.6700664542332742]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn ipop_cma_es_async_matches_sync() {
let cfg = IpopCmaEsConfig {
initial_population_size: 4,
total_generations: 40,
initial_sigma: 0.5,
eigen_decomposition_period: 1,
stall_generations: None,
seed: 42,
};
let mut a = IpopCmaEs::new(cfg.clone(), snap_so_bounds());
let mut b = IpopCmaEs::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![0.9074496962843028]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn separable_nes_async_matches_sync() {
let cfg = SeparableNesConfig {
population_size: 6,
generations: 25,
initial_sigma: 0.5,
mean_learning_rate: 1.0,
sigma_learning_rate: Some(0.1),
seed: 42,
};
let mut a = SeparableNes::new(cfg.clone(), snap_so_bounds());
let mut b = SeparableNes::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![2.3348936837614187]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn tlbo_async_matches_sync() {
let cfg = TlboConfig {
population_size: 6,
generations: 25,
seed: 42,
};
let mut a = Tlbo::new(cfg.clone(), snap_so_bounds());
let mut b = Tlbo::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.22584258931975904],
vec![0.287744793845985],
vec![0.2909810385734612],
vec![0.3033740027429419],
vec![0.32232945704973176],
vec![0.32269000540663206]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn nelder_mead_async_matches_sync() {
let cfg = NelderMeadConfig {
iterations: 40,
..NelderMeadConfig::default()
};
let mut a = NelderMead::new(cfg.clone(), snap_so_bounds());
let mut b = NelderMead::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![0.37482713384688104]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn bayesian_opt_async_matches_sync() {
let cfg = BayesianOptConfig {
initial_samples: 3,
iterations: 12,
length_scales: None,
signal_variance: 1.0,
noise_variance: 1e-3,
acquisition_samples: 8,
seed: 42,
};
let mut a = BayesianOpt::new(cfg.clone(), snap_so_bounds());
let mut b = BayesianOpt::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![37.636221207678574],
vec![63.306134606086815],
vec![330.7204672154741],
vec![420.2872728247263],
vec![469.01167857530334],
vec![683.620651365178],
vec![1296.331547643922],
vec![1975.425113659962],
vec![2308.580211846731],
vec![4362.422716839316],
vec![4781.49796690649],
vec![5246.461124345242],
vec![5726.51240444759],
vec![7901.947292257745],
vec![10608.203503373621]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn tpe_async_matches_sync() {
let cfg = TpeConfig {
initial_samples: 3,
iterations: 12,
good_fraction: 0.25,
candidate_samples: 8,
bandwidth_factor: 0.1,
seed: 42,
};
let mut a = Tpe::new(cfg.clone(), snap_so_bounds());
let mut b = Tpe::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSphere);
let r_async = b.run_async(&SnapSphere, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![63.306109614392106],
vec![63.306116949436515],
vec![63.306117012658824],
vec![63.30611956234049],
vec![63.30612002408215],
vec![63.30612002731914],
vec![63.30612144250236],
vec![63.30613258185937],
vec![63.30613285480626],
vec![63.306134606086815],
vec![63.306138944169454],
vec![63.306140428140985],
vec![63.30614044375412],
vec![4362.422716839316],
vec![7901.947292257745]
]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
// --- Multi-objective: best-comparison falls back to pareto front size ----
//
// For multi-objective algorithms, `best` is only meaningful as
// `best_by_some_scalarization`. We compare the sorted Pareto-front
// objective tuples instead.
fn front_objectives<D>(r: &OptimizationResult<D>) -> Vec<Vec<f64>> {
let mut front: Vec<Vec<f64>> = r
.pareto_front
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
front.sort_by(|a, b| {
for (x, y) in a.iter().zip(b.iter()) {
match x.partial_cmp(y) {
Some(std::cmp::Ordering::Equal) => continue,
Some(ord) => return ord,
None => return std::cmp::Ordering::Equal,
}
}
std::cmp::Ordering::Equal
});
front
}
/// Sorted objective tuples of the *entire final population* — far more
/// mutation-sensitive than `best` alone, since a mutated update rule
/// changes the search distribution (and hence the sampled population)
/// even when the single best-ever point happens to be unchanged.
/// Falls back to the best candidate if the algorithm leaves
/// `population` empty.
fn population_objectives<D>(r: &OptimizationResult<D>) -> Vec<Vec<f64>> {
let mut pop: Vec<Vec<f64>> = r
.population
.iter()
.map(|c| c.evaluation.objectives.clone())
.collect();
if pop.is_empty() {
if let Some(best) = r.best.as_ref() {
pop.push(best.evaluation.objectives.clone());
}
}
pop.sort_by(|a, b| {
for (x, y) in a.iter().zip(b.iter()) {
match x.partial_cmp(y) {
Some(std::cmp::Ordering::Equal) => continue,
Some(ord) => return ord,
None => return std::cmp::Ordering::Equal,
}
}
std::cmp::Ordering::Equal
});
pop
}
#[tokio::test]
async fn nsga2_async_matches_sync() {
let make = || {
Nsga2::new(
Nsga2Config {
population_size: 8,
generations: 25,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.013894099789814499, 12.396569748512157],
vec![0.9986044047366547, 10.04928885630924],
vec![1.4179339198509142, 5.938538752276063],
vec![1.9377635550108065, 5.175062400323034],
vec![4.287814676408594, 2.072533399238466],
vec![6.144590989246883, 0.9919485019724119],
vec![7.568542905344836, 0.8775124508515268],
vec![11.162223470843461, 0.06736833759926654]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn nsga3_async_matches_sync() {
let make = || {
Nsga3::new(
Nsga3Config {
population_size: 8,
generations: 25,
reference_divisions: 4,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.04207557391436843, 11.452614567675102],
vec![0.5483942154144871, 8.561837256536368],
vec![1.547355561053402, 5.037243897511284],
vec![2.1897000988710995, 4.570866266616154],
vec![2.6617298789502133, 3.4774928534855687],
vec![4.557616972017328, 2.413271056308534],
vec![8.18734188319979, 0.40562353775794924],
vec![11.538740749611467, 0.20265277704364293]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn spea2_async_matches_sync() {
let make = || {
Spea2::new(
Spea2Config {
population_size: 8,
archive_size: 4,
generations: 25,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.010459871966689165, 11.383115832165583],
vec![1.00406197192257, 6.710772782810457],
vec![4.617743017065397, 2.193494053600796],
vec![11.716970274948123, 0.016460995225913946]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn moead_async_matches_sync() {
let make = || {
Moead::new(
MoeadConfig {
generations: 25,
reference_divisions: 4,
neighborhood_size: 2,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.0018965941331487578, 11.808507055951427],
vec![1.4874217300175174, 5.27715535815546],
vec![2.947894875106906, 3.446066071999487],
vec![4.30232389012548, 1.949958003829106],
vec![9.43090543273526, 0.6038381317627364]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn mopso_async_matches_sync() {
let make = || {
Mopso::new(
MopsoConfig {
swarm_size: 6,
generations: 25,
archive_size: 4,
inertia: 0.5,
cognitive: 1.0,
social: 1.0,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![1.209299108345119, 5.907064121476177],
vec![1.5729555505269657, 4.991703224346624],
vec![1.9490201701027443, 4.316356698004658],
vec![1.9596051383622117, 4.292838266270067]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn ibea_async_matches_sync() {
let make = || {
Ibea::new(
IbeaConfig {
population_size: 8,
generations: 25,
kappa: 0.05,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![1.102989174772048, 5.927013751828334],
vec![1.6270095299640162, 4.827939086661083],
vec![2.1660395399724406, 4.144479169637154],
vec![2.75551550490187, 3.2592048793171986],
vec![3.4190662252024984, 2.638232122470969],
vec![5.096097851324554, 1.4897953804615525],
vec![6.191588790986529, 1.076799546289505],
vec![10.471191707237805, 0.19856457625474933]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn sms_emoa_async_matches_sync() {
let make = || {
SmsEmoa::new(
SmsEmoaConfig {
population_size: 8,
generations: 25,
reference_point: vec![100.0, 100.0],
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.48629723752715737, 9.65467157344917],
vec![0.76152466146821, 7.823579428272707],
vec![1.013464442017104, 6.402048672524341],
vec![1.2493755385572065, 6.77907250568554],
vec![2.23189833205296, 5.88957515209449],
vec![3.8980878113466164, 5.570985049586769],
vec![4.442278547014425, 3.495601181208072],
vec![5.04184251314055, 4.67520303050857]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn hype_async_matches_sync() {
let make = || {
Hype::new(
HypeConfig {
population_size: 8,
generations: 25,
reference_point: vec![10.0, 10.0],
mc_samples: 4,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![2.1468356594601867, 4.082170172701482],
vec![2.258243590202526, 3.9253070443424924],
vec![2.438385974767159, 3.681728006983862],
vec![3.3195313427544137, 3.197540830543744],
vec![3.471491609011644, 2.6174420196371186],
vec![3.8982204071014666, 2.61118163669461],
vec![3.9359534122250466, 2.515067124016713],
vec![4.040911931124477, 2.1294731723321547]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn pesa_ii_async_matches_sync() {
let make = || {
PesaII::new(
PesaIIConfig {
population_size: 8,
archive_size: 4,
generations: 25,
grid_divisions: 4,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![3.6245079224196353, 5.073627020565048],
vec![5.546147318944607, 1.8534963745959785],
vec![6.526086710933148, 1.0660738392669853],
vec![10.515670253117545, 0.12279280481335361]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn epsilon_moea_async_matches_sync() {
let make = || {
EpsilonMoea::new(
EpsilonMoeaConfig {
population_size: 8,
evaluations: 12,
epsilon: vec![0.1, 0.1],
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.76152466146821, 7.823579428272707],
vec![2.2218156464658056, 5.784958465320774]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn age_moea_async_matches_sync() {
let make = || {
AgeMoea::new(
AgeMoeaConfig {
population_size: 8,
generations: 25,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.04111219105652014, 13.074348360880695],
vec![0.4346731920812637, 9.242155513828948],
vec![1.358331428580249, 5.683016286040829],
vec![2.5755814019595404, 3.60622785688025],
vec![3.837138107106994, 2.3180546082918916],
vec![5.286102901854588, 1.4045244018265168],
vec![7.638613726361841, 0.9440692528451459],
vec![11.019174430211159, 0.15681647211812566]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn grea_async_matches_sync() {
let make = || {
Grea::new(
GreaConfig {
population_size: 8,
generations: 25,
grid_divisions: 4,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.5552666375678257, 7.47469613619204],
vec![1.4056896475138974, 5.293501721710313],
vec![1.5852083174630922, 4.882658343892137],
vec![1.7395860023245642, 4.82943787827538],
vec![1.7409421319942333, 4.813466888123339],
vec![2.055412878673668, 4.334431622012692],
vec![2.7400972765938705, 3.784963007905067],
vec![2.968002937322503, 3.3418351793574135]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn knea_async_matches_sync() {
let make = || {
Knea::new(
KneaConfig {
population_size: 8,
generations: 25,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.0018483545960612325, 12.22673649645838],
vec![0.017237011688299664, 11.387998693142336],
vec![0.027973368060834943, 10.968779947026622],
vec![0.04220440957898664, 10.726923433631885],
vec![0.04349788464967149, 10.665580898567551],
vec![0.08092072017224045, 10.438020089521329],
vec![0.12927891629102406, 10.135723096330521],
vec![0.13777903061778596, 9.820391893329669]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn rvea_async_matches_sync() {
let make = || {
Rvea::new(
RveaConfig {
population_size: 8,
generations: 25,
reference_divisions: 4,
alpha: 2.0,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
snap_mo_variation(),
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.7822578730650152, 6.840627682227026],
vec![1.886520215889041, 4.4221143878379205],
vec![2.925708852333473, 3.0764597322421308],
vec![2.925708852333473, 3.0764597322421308],
vec![2.925708852333473, 3.0764597322421308],
vec![2.925708852333473, 3.0764597322421308],
vec![4.2172159905416375, 2.2535573427617255],
vec![5.629611931678675, 1.2046715353568256]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
#[tokio::test]
async fn paes_async_matches_sync() {
let make = || {
Paes::new(
PaesConfig {
iterations: 40,
archive_size: 4,
seed: 42,
},
RealBounds::new(snap_mo_bounds()),
GaussianMutation { sigma: 0.1 },
)
};
let r_sync = make().run(&SnapMo);
let r_async = make().run_async(&SnapMo, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![
vec![0.40915759624698217, 8.374880286293996],
vec![0.5346610020355553, 8.30481689014011],
vec![0.5822402093594947, 7.942141169439722],
vec![0.6766093633299375, 7.383787211092805]
]
);
assert_eq!(front_objectives(&r_sync), front_objectives(&r_async));
}
// --- Binary, integer, permutation, multi-fidelity ----------------------
#[tokio::test]
async fn umda_async_matches_sync() {
let cfg = UmdaConfig {
bits: 4,
population_size: 6,
selected_size: 3,
generations: 25,
seed: 42,
};
let mut a = Umda::new(cfg.clone());
let mut b = Umda::new(cfg);
let problem = OneMax { bits: 4 };
let r_sync = a.run(&problem);
let r_async = b.run_async(&problem, 2).await;
assert_eq!(population_objectives(&r_sync), vec![vec![4.0]]);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn ant_colony_tsp_async_matches_sync() {
let problem = TinyTsp::new();
let cfg = AntColonyTspConfig {
ants: 4,
generations: 25,
alpha: 1.0,
beta: 2.0,
evaporation: 0.5,
deposit: 1.0,
initial_pheromone: 1.0,
seed: 42,
};
let mut a = AntColonyTsp::new(cfg.clone(), problem.dist.clone());
let mut b = AntColonyTsp::new(cfg, problem.dist.clone());
let r_sync = a.run(&problem);
let r_async = b.run_async(&problem, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![19.16243758807328]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn tabu_search_async_matches_sync() {
struct StartAt5;
impl Initializer<Vec<i32>> for StartAt5 {
fn initialize(
&mut self,
_size: usize,
_rng: &mut heuropt::core::rng::Rng,
) -> Vec<Vec<i32>> {
vec![vec![5]]
}
}
let neighbors =
|x: &Vec<i32>, _rng: &mut heuropt::core::rng::Rng| vec![vec![x[0] - 1], vec![x[0] + 1]];
let cfg = TabuSearchConfig {
iterations: 40,
tabu_tenure: 3,
seed: 42,
};
let mut a = TabuSearch::new(cfg.clone(), StartAt5, neighbors);
let mut b = TabuSearch::new(cfg, StartAt5, neighbors);
let r_sync = a.run(&AbsInt);
let r_async = b.run_async(&AbsInt, 2).await;
assert_eq!(population_objectives(&r_sync), vec![vec![0.0]]);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
#[tokio::test]
async fn hyperband_async_matches_sync() {
let cfg = HyperbandConfig {
max_budget: 27.0,
eta: 3.0,
max_brackets: 2,
seed: 42,
};
let mut a: Hyperband<RealBounds, Vec<f64>> = Hyperband::new(cfg.clone(), snap_so_bounds());
let mut b: Hyperband<RealBounds, Vec<f64>> = Hyperband::new(cfg, snap_so_bounds());
let r_sync = a.run(&SnapSpherePartial);
let r_async = b.run_async(&SnapSpherePartial, 2).await;
assert_eq!(
population_objectives(&r_sync),
vec![vec![65.59222036219585]]
);
assert_eq!(objectives_of(&r_sync), objectives_of(&r_async));
}
}