test(sms_emoa,pesa2,paes,random_search): pin remaining algorithm helpers
Phase 1, final algorithm batch: - sms_emoa: pick_drop_index returns the singleton worst front, and finds the least-HV-contributor at a non-zero index. - pesa2: build_grid empty/corner-point boxing; region_tournament prefers the less-crowded grid box (statistical majority). - paes: deterministic non-empty front + archive cap. - random_search: evaluation count = iterations*batch; best is no worse than any sampled candidate.
This commit is contained in:
@@ -305,4 +305,26 @@ mod tests {
|
|||||||
let r = opt.run(&Sphere1D);
|
let r = opt.run(&Sphere1D);
|
||||||
assert!(r.best.is_some());
|
assert!(r.best.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PAES must return a non-empty Pareto archive on a 2-objective problem
|
||||||
|
/// and be deterministic with a fixed seed. Pins the run-loop
|
||||||
|
/// bookkeeping against degenerate / comparison mutants.
|
||||||
|
#[test]
|
||||||
|
fn produces_deterministic_nonempty_front() {
|
||||||
|
let make = || {
|
||||||
|
Paes::new(
|
||||||
|
PaesConfig { iterations: 40, archive_size: 10, seed: 5 },
|
||||||
|
RealBounds::new(vec![(-5.0, 5.0)]),
|
||||||
|
GaussianMutation { sigma: 0.3 },
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let r1 = make().run(&SchafferN1);
|
||||||
|
let r2 = make().run(&SchafferN1);
|
||||||
|
assert!(!r1.pareto_front.is_empty());
|
||||||
|
let f1: Vec<Vec<f64>> = r1.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect();
|
||||||
|
let f2: Vec<Vec<f64>> = r2.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect();
|
||||||
|
assert_eq!(f1, f2);
|
||||||
|
// Archive never exceeds its configured cap.
|
||||||
|
assert!(r1.pareto_front.len() <= 10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -499,4 +499,67 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let _ = opt.run(&SchafferN1);
|
let _ = opt.run(&SchafferN1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Mutation-test pinned helpers --------------------------------------
|
||||||
|
|
||||||
|
use crate::core::candidate::Candidate;
|
||||||
|
use crate::core::evaluation::Evaluation;
|
||||||
|
use crate::core::objective::{Objective, ObjectiveSpace};
|
||||||
|
|
||||||
|
fn space2() -> ObjectiveSpace {
|
||||||
|
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_grid_empty_archive_is_empty() {
|
||||||
|
let archive = ParetoArchive::<u32>::new(space2());
|
||||||
|
let (boxes, counts) = build_grid(&archive, &space2(), 4);
|
||||||
|
assert!(boxes.is_empty());
|
||||||
|
assert!(counts.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_grid_assigns_corner_points_to_distinct_boxes() {
|
||||||
|
let mut archive = ParetoArchive::<u32>::new(space2());
|
||||||
|
// Three non-dominated corner points span the grid extremes.
|
||||||
|
archive.insert(Candidate::new(1u32, Evaluation::new(vec![0.0, 4.0])));
|
||||||
|
archive.insert(Candidate::new(2u32, Evaluation::new(vec![2.0, 2.0])));
|
||||||
|
archive.insert(Candidate::new(3u32, Evaluation::new(vec![4.0, 0.0])));
|
||||||
|
let (boxes, counts) = build_grid(&archive, &space2(), 4);
|
||||||
|
assert_eq!(boxes.len(), 3);
|
||||||
|
// The min and max corners land in different boxes — total count
|
||||||
|
// across all boxes equals the member count.
|
||||||
|
let total: usize = counts.values().sum();
|
||||||
|
assert_eq!(total, 3);
|
||||||
|
// The two extreme points are in different boxes (grid spreads them).
|
||||||
|
assert_ne!(boxes[0], boxes[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn region_tournament_prefers_less_crowded_box() {
|
||||||
|
use crate::core::rng::rng_from_seed;
|
||||||
|
// Members 0 and 1 share a crowded box (count 2); member 2 is alone.
|
||||||
|
let mut archive = ParetoArchive::<u32>::new(space2());
|
||||||
|
archive.insert(Candidate::new(1u32, Evaluation::new(vec![0.0, 4.0])));
|
||||||
|
archive.insert(Candidate::new(2u32, Evaluation::new(vec![2.0, 2.0])));
|
||||||
|
archive.insert(Candidate::new(3u32, Evaluation::new(vec![4.0, 0.0])));
|
||||||
|
// Hand-build boxes/counts where index 2 is in a singleton box and
|
||||||
|
// indices 0,1 share a crowded box.
|
||||||
|
let boxes = vec![vec![0usize, 0], vec![0usize, 0], vec![3usize, 3]];
|
||||||
|
let mut counts = std::collections::BTreeMap::new();
|
||||||
|
counts.insert(vec![0usize, 0], 2usize);
|
||||||
|
counts.insert(vec![3usize, 3], 1usize);
|
||||||
|
// Across many seeds, the less-crowded index (2) must win whenever
|
||||||
|
// the two random draws differ between the crowded/uncrowded boxes.
|
||||||
|
let mut picked_uncrowded = 0;
|
||||||
|
for seed in 0..300 {
|
||||||
|
let mut rng = rng_from_seed(seed);
|
||||||
|
if region_tournament(&archive, &boxes, &counts, &mut rng) == 2 {
|
||||||
|
picked_uncrowded += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Index 2 wins whenever it's drawn against 0 or 1, plus half its
|
||||||
|
// self-draws — clear majority.
|
||||||
|
assert!(picked_uncrowded > 150, "uncrowded picked {picked_uncrowded}/300");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,4 +218,23 @@ mod tests {
|
|||||||
let r = opt.run(&Sphere1D);
|
let r = opt.run(&Sphere1D);
|
||||||
assert!(r.best.is_some());
|
assert!(r.best.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RandomSearch's evaluation count is exactly `iterations * batch_size`,
|
||||||
|
/// and the returned best is no worse than every sampled candidate.
|
||||||
|
#[test]
|
||||||
|
fn best_is_no_worse_than_any_sample() {
|
||||||
|
let mut opt = RandomSearch::new(
|
||||||
|
RandomSearchConfig { iterations: 50, batch_size: 2, seed: 9 },
|
||||||
|
RealBounds::new(vec![(-3.0, 3.0)]),
|
||||||
|
);
|
||||||
|
let r = opt.run(&Sphere1D);
|
||||||
|
assert_eq!(r.evaluations, 100);
|
||||||
|
let best = r.best.unwrap().evaluation.objectives[0];
|
||||||
|
let pop_min = r
|
||||||
|
.population
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.evaluation.objectives[0])
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
assert!(best <= pop_min + 1e-12, "best {best} > pop min {pop_min}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -400,4 +400,51 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let _ = opt.run(&SchafferN1);
|
let _ = opt.run(&SchafferN1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Mutation-test pinned helpers --------------------------------------
|
||||||
|
|
||||||
|
use crate::core::candidate::Candidate;
|
||||||
|
use crate::core::evaluation::Evaluation;
|
||||||
|
use crate::core::objective::{Objective, ObjectiveSpace};
|
||||||
|
|
||||||
|
fn sms_space() -> ObjectiveSpace {
|
||||||
|
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
|
||||||
|
}
|
||||||
|
fn sms_cand(o: Vec<f64>) -> Candidate<u32> {
|
||||||
|
Candidate::new(0, Evaluation::new(o))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `pick_drop_index` drops the member of the worst front with the
|
||||||
|
/// smallest hypervolume contribution. With one clearly-dominated point
|
||||||
|
/// in the pool, that point forms a singleton worst front and is
|
||||||
|
/// returned directly.
|
||||||
|
#[test]
|
||||||
|
fn pick_drop_index_returns_singleton_worst_front() {
|
||||||
|
// (1,1) and (2,2)-trade-offs are front 0; (9,9) is dominated → the
|
||||||
|
// sole member of front 1.
|
||||||
|
let pool = vec![
|
||||||
|
sms_cand(vec![1.0, 3.0]),
|
||||||
|
sms_cand(vec![3.0, 1.0]),
|
||||||
|
sms_cand(vec![9.0, 9.0]), // dominated — worst front, singleton
|
||||||
|
];
|
||||||
|
let drop = pick_drop_index(&pool, &sms_space(), &[100.0, 100.0]);
|
||||||
|
assert_eq!(drop, 2, "should drop the dominated singleton");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the worst front has multiple members, the one with the
|
||||||
|
/// smallest hypervolume contribution is dropped — and the scan must
|
||||||
|
/// find it even at a non-zero index. Here `(1.0, 9.0)` at index 1 is
|
||||||
|
/// "shadowed" by its near-neighbour `(1.5, 8.5)` and contributes the
|
||||||
|
/// least unique HV (≈ 0.5 vs ≈ 3.75 and ≈ 7.5).
|
||||||
|
#[test]
|
||||||
|
fn pick_drop_index_drops_least_hv_contributor() {
|
||||||
|
// All three mutually non-dominated → single (worst) front.
|
||||||
|
let pool = vec![
|
||||||
|
sms_cand(vec![1.5, 8.5]),
|
||||||
|
sms_cand(vec![1.0, 9.0]), // least HV contribution → drop target
|
||||||
|
sms_cand(vec![9.0, 1.0]),
|
||||||
|
];
|
||||||
|
let drop = pick_drop_index(&pool, &sms_space(), &[10.0, 10.0]);
|
||||||
|
assert_eq!(drop, 1, "should drop the lowest-HV-contribution member");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user