use std::collections::BTreeMap;
use rand::Rng as _;
use crate::core::candidate::Candidate;
use crate::core::objective::ObjectiveSpace;
use crate::core::population::Population;
use crate::core::problem::Problem;
use crate::core::result::OptimizationResult;
use crate::core::rng::{Rng, rng_from_seed};
use crate::pareto::archive::ParetoArchive;
use crate::pareto::front::{best_candidate, pareto_front};
use crate::traits::{Initializer, Optimizer, Variation};
#[derive(Debug, Clone)]
pub struct PesaIIConfig {
pub population_size: usize,
pub archive_size: usize,
pub generations: usize,
pub grid_divisions: usize,
pub seed: u64,
}
impl Default for PesaIIConfig {
fn default() -> Self {
Self {
population_size: 50,
archive_size: 100,
generations: 250,
grid_divisions: 16,
seed: 42,
}
}
}
#[derive(Debug, Clone)]
pub struct PesaII<I, V> {
pub config: PesaIIConfig,
pub initializer: I,
pub variation: V,
}
impl<I, V> PesaII<I, V> {
pub fn new(config: PesaIIConfig, initializer: I, variation: V) -> Self {
Self {
config,
initializer,
variation,
}
}
}
impl<P, I, V> Optimizer<P> for PesaII<I, V>
where
P: Problem + Sync,
P::Decision: Send,
I: Initializer<P::Decision>,
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"
);
let n = self.config.population_size;
let objectives = problem.objectives();
let mut rng = rng_from_seed(self.config.seed);
let initial_decisions = self.initializer.initialize(n, &mut rng);
let mut internal: Vec<Candidate<P::Decision>> = initial_decisions
.into_iter()
.map(|d| {
let e = problem.evaluate(&d);
Candidate::new(d, e)
})
.collect();
let mut evaluations = internal.len();
let mut archive = ParetoArchive::new(objectives.clone());
for c in &internal {
archive.insert(c.clone());
}
truncate_by_grid(
&mut archive,
self.config.archive_size,
self.config.grid_divisions,
);
for _ in 0..self.config.generations {
let (boxes, counts) = build_grid(&archive, &objectives, self.config.grid_divisions);
let mut offspring: Vec<Candidate<P::Decision>> = Vec::with_capacity(n);
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 children = self.variation.vary(&parents, &mut rng);
assert!(
!children.is_empty(),
"PesaII variation returned no children"
);
for child in children {
if offspring.len() >= n {
break;
}
let eval = problem.evaluate(&child);
evaluations += 1;
offspring.push(Candidate::new(child, eval));
}
}
for c in &offspring {
archive.insert(c.clone());
}
truncate_by_grid(
&mut archive,
self.config.archive_size,
self.config.grid_divisions,
);
internal = offspring;
}
let _ = internal; let members = archive.into_vec();
let front = pareto_front(&members, &objectives);
let best = best_candidate(&members, &objectives);
OptimizationResult::new(
Population::new(members),
front,
best,
evaluations,
self.config.generations,
)
}
}
fn build_grid<D: Clone>(
archive: &ParetoArchive<D>,
objectives: &ObjectiveSpace,
divisions: usize,
) -> (Vec<Vec<usize>>, BTreeMap<Vec<usize>, usize>) {
let m = objectives.len();
let members = archive.members();
if members.is_empty() {
return (Vec::new(), BTreeMap::new());
}
let oriented: Vec<Vec<f64>> = members
.iter()
.map(|c| objectives.as_minimization(&c.evaluation.objectives))
.collect();
let mut lo = vec![f64::INFINITY; m];
let mut hi = vec![f64::NEG_INFINITY; m];
for o in &oriented {
for k in 0..m {
if o[k] < lo[k] {
lo[k] = o[k];
}
if o[k] > hi[k] {
hi[k] = o[k];
}
}
}
let mut boxes: Vec<Vec<usize>> = Vec::with_capacity(members.len());
for o in &oriented {
let mut box_idx = Vec::with_capacity(m);
for k in 0..m {
let span = (hi[k] - lo[k]).max(1e-12);
let frac = ((o[k] - lo[k]) / span).clamp(0.0, 1.0 - 1e-9);
box_idx.push((frac * divisions as f64) as usize);
}
boxes.push(box_idx);
}
let mut counts: BTreeMap<Vec<usize>, usize> = BTreeMap::new();
for b in &boxes {
*counts.entry(b.clone()).or_insert(0) += 1;
}
(boxes, counts)
}
fn region_tournament<D: Clone>(
archive: &ParetoArchive<D>,
boxes: &[Vec<usize>],
counts: &BTreeMap<Vec<usize>, usize>,
rng: &mut Rng,
) -> usize {
let n = archive.members().len();
let a = rng.random_range(0..n);
let b = rng.random_range(0..n);
let ca = counts.get(&boxes[a]).copied().unwrap_or(1);
let cb = counts.get(&boxes[b]).copied().unwrap_or(1);
if ca < cb {
a
} else if cb < ca {
b
} else if rng.random_bool(0.5) {
a
} else {
b
}
}
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);
let max_count = counts.values().copied().max().unwrap_or(0);
if max_count <= 1 {
archive.truncate(max_size);
break;
}
let crowded_box = counts
.iter()
.find(|&(_, &c)| c == max_count)
.map(|(b, _)| b.clone())
.unwrap();
let candidates: Vec<usize> = boxes
.iter()
.enumerate()
.filter(|(_, b)| **b == crowded_box)
.map(|(i, _)| i)
.collect();
let evict = *candidates.first().expect("non-empty crowded box");
archive.members.swap_remove(evict);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::operators::{
CompositeVariation, PolynomialMutation, RealBounds, SimulatedBinaryCrossover,
};
use crate::tests_support::SchafferN1;
fn make_optimizer(
seed: u64,
) -> PesaII<RealBounds, CompositeVariation<SimulatedBinaryCrossover, PolynomialMutation>> {
let bounds = vec![(-5.0, 5.0)];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
};
PesaII::new(
PesaIIConfig {
population_size: 20,
archive_size: 30,
generations: 15,
grid_divisions: 8,
seed,
},
initializer,
variation,
)
}
#[test]
fn produces_pareto_front() {
let mut opt = make_optimizer(1);
let r = opt.run(&SchafferN1);
assert!(!r.pareto_front.is_empty());
}
#[test]
fn deterministic_with_same_seed() {
let mut a = make_optimizer(99);
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();
assert_eq!(oa, ob);
}
#[test]
#[should_panic(expected = "archive_size must be > 0")]
fn zero_archive_size_panics() {
let bounds = vec![(0.0, 1.0)];
let initializer = RealBounds::new(bounds.clone());
let variation = CompositeVariation {
crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5),
mutation: PolynomialMutation::new(bounds, 20.0, 1.0),
};
let mut opt = PesaII::new(
PesaIIConfig {
population_size: 4,
archive_size: 0,
generations: 1,
grid_divisions: 4,
seed: 0,
},
initializer,
variation,
);
let _ = opt.run(&SchafferN1);
}
}