diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs
index 373701c..76e15a8 100644
--- a/src/algorithms/mod.rs
+++ b/src/algorithms/mod.rs
@@ -1,5 +1,7 @@
//! Built-in reference optimizers.
+pub mod paes;
pub mod random_search;
+pub use paes::*;
pub use random_search::*;
diff --git a/src/algorithms/paes.rs b/src/algorithms/paes.rs
new file mode 100644
index 0000000..110c7f3
--- /dev/null
+++ b/src/algorithms/paes.rs
@@ -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 {
+ /// 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 Paes {
+ /// Construct a `Paes` optimizer.
+ pub fn new(config: PaesConfig, initializer: I, variation: V) -> Self {
+ Self { config, initializer, variation }
+ }
+}
+
+impl
Optimizer
for Paes
+where
+ P: Problem,
+ I: Initializer,
+ V: Variation,
+{
+ fn run(&mut self, problem: &P) -> OptimizationResult {
+ 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());
+ }
+}
diff --git a/src/prelude.rs b/src/prelude.rs
index 51ad9e1..a3cb6c6 100644
--- a/src/prelude.rs
+++ b/src/prelude.rs
@@ -18,4 +18,4 @@ pub use crate::pareto::{
pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};
-pub use crate::algorithms::{RandomSearch, RandomSearchConfig};
+pub use crate::algorithms::{Paes, PaesConfig, RandomSearch, RandomSearchConfig};