feat(algorithms): add Paes (Pareto Archived Evolution Strategy)
A readable v1 PAES (spec §12.2): - Single starting decision from the initializer. - Each iteration mutates the current decision via the Variation operator, evaluates the child, and pareto_compares to the current. - Dominating children become current; for non-dominated comparisons we move to the child (acceptable v1 behavior per spec). - Both current and child are inserted into a ParetoArchive truncated to `archive_size` (simple tail-truncation in v1). The final result returns the archive as both `population` and `pareto_front`. Tests verify the archive never exceeds `archive_size`.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
//! Built-in reference optimizers.
|
//! Built-in reference optimizers.
|
||||||
|
|
||||||
|
pub mod paes;
|
||||||
pub mod random_search;
|
pub mod random_search;
|
||||||
|
|
||||||
|
pub use paes::*;
|
||||||
pub use random_search::*;
|
pub use random_search::*;
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
//! Pareto Archived Evolution Strategy — a small (1+1)-with-archive optimizer.
|
||||||
|
|
||||||
|
use crate::core::candidate::Candidate;
|
||||||
|
use crate::core::population::Population;
|
||||||
|
use crate::core::problem::Problem;
|
||||||
|
use crate::core::result::OptimizationResult;
|
||||||
|
use crate::core::rng::rng_from_seed;
|
||||||
|
use crate::pareto::archive::ParetoArchive;
|
||||||
|
use crate::pareto::dominance::{Dominance, pareto_compare};
|
||||||
|
use crate::pareto::front::{best_candidate, pareto_front};
|
||||||
|
use crate::traits::{Initializer, Optimizer, Variation};
|
||||||
|
|
||||||
|
/// Configuration for [`Paes`].
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PaesConfig {
|
||||||
|
/// Number of mutation iterations.
|
||||||
|
pub iterations: usize,
|
||||||
|
/// Maximum size of the Pareto archive.
|
||||||
|
pub archive_size: usize,
|
||||||
|
/// Seed for the deterministic RNG.
|
||||||
|
pub seed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PaesConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { iterations: 1000, archive_size: 100, seed: 42 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple Pareto Archived Evolution Strategy.
|
||||||
|
///
|
||||||
|
/// One current candidate, one mutation per iteration, one bounded archive.
|
||||||
|
/// Intentionally a readable baseline rather than a research-perfect PAES
|
||||||
|
/// (spec §12.2).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Paes<I, V> {
|
||||||
|
/// Algorithm configuration.
|
||||||
|
pub config: PaesConfig,
|
||||||
|
/// How the initial decision is sampled.
|
||||||
|
pub initializer: I,
|
||||||
|
/// How children are produced from the current decision.
|
||||||
|
pub variation: 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, I, V> Optimizer<P> for Paes<I, V>
|
||||||
|
where
|
||||||
|
P: Problem,
|
||||||
|
I: Initializer<P::Decision>,
|
||||||
|
V: Variation<P::Decision>,
|
||||||
|
{
|
||||||
|
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
|
||||||
|
assert!(
|
||||||
|
self.config.archive_size > 0,
|
||||||
|
"PAES archive_size must be greater than 0",
|
||||||
|
);
|
||||||
|
|
||||||
|
let objectives = problem.objectives();
|
||||||
|
let mut rng = rng_from_seed(self.config.seed);
|
||||||
|
|
||||||
|
let mut initial = self.initializer.initialize(1, &mut rng);
|
||||||
|
assert!(
|
||||||
|
!initial.is_empty(),
|
||||||
|
"PAES initializer returned no decisions",
|
||||||
|
);
|
||||||
|
let mut current_decision = initial.remove(0);
|
||||||
|
let mut current_eval = problem.evaluate(¤t_decision);
|
||||||
|
let mut evaluations = 1usize;
|
||||||
|
|
||||||
|
let mut archive = ParetoArchive::new(objectives.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",
|
||||||
|
);
|
||||||
|
let child_decision = children.into_iter().next().unwrap();
|
||||||
|
let child_eval = problem.evaluate(&child_decision);
|
||||||
|
evaluations += 1;
|
||||||
|
|
||||||
|
match pareto_compare(&child_eval, ¤t_eval, &objectives) {
|
||||||
|
Dominance::Dominates => {
|
||||||
|
current_decision = child_decision.clone();
|
||||||
|
current_eval = child_eval.clone();
|
||||||
|
}
|
||||||
|
Dominance::DominatedBy => {
|
||||||
|
// Stay at current.
|
||||||
|
}
|
||||||
|
Dominance::NonDominated | Dominance::Equal => {
|
||||||
|
// v1: move to child on non-dominated comparison.
|
||||||
|
current_decision = child_decision.clone();
|
||||||
|
current_eval = child_eval.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.insert(Candidate::new(child_decision, child_eval));
|
||||||
|
archive.insert(Candidate::new(current_decision.clone(), current_eval.clone()));
|
||||||
|
archive.truncate(self.config.archive_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.iterations,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::operators::{GaussianMutation, RealBounds};
|
||||||
|
use crate::tests_support::{SchafferN1, Sphere1D};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn produces_at_least_one_candidate() {
|
||||||
|
let mut opt = Paes::new(
|
||||||
|
PaesConfig { iterations: 50, archive_size: 16, seed: 1 },
|
||||||
|
RealBounds::new(vec![(-5.0, 5.0)]),
|
||||||
|
GaussianMutation { sigma: 0.3 },
|
||||||
|
);
|
||||||
|
let r = opt.run(&SchafferN1);
|
||||||
|
assert!(!r.population.is_empty());
|
||||||
|
assert!(!r.pareto_front.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn archive_size_respected() {
|
||||||
|
let mut opt = Paes::new(
|
||||||
|
PaesConfig { iterations: 200, archive_size: 8, seed: 2 },
|
||||||
|
RealBounds::new(vec![(-5.0, 5.0)]),
|
||||||
|
GaussianMutation { sigma: 0.2 },
|
||||||
|
);
|
||||||
|
let r = opt.run(&SchafferN1);
|
||||||
|
assert!(r.population.len() <= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_objective_returns_best() {
|
||||||
|
let mut opt = Paes::new(
|
||||||
|
PaesConfig { iterations: 200, archive_size: 8, seed: 3 },
|
||||||
|
RealBounds::new(vec![(-2.0, 2.0)]),
|
||||||
|
GaussianMutation { sigma: 0.1 },
|
||||||
|
);
|
||||||
|
let r = opt.run(&Sphere1D);
|
||||||
|
assert!(r.best.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -18,4 +18,4 @@ pub use crate::pareto::{
|
|||||||
|
|
||||||
pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};
|
pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};
|
||||||
|
|
||||||
pub use crate::algorithms::{RandomSearch, RandomSearchConfig};
|
pub use crate::algorithms::{Paes, PaesConfig, RandomSearch, RandomSearchConfig};
|
||||||
|
|||||||
Reference in New Issue
Block a user