diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs
index ff3803a..b2840c1 100644
--- a/src/algorithms/mod.rs
+++ b/src/algorithms/mod.rs
@@ -15,6 +15,7 @@ pub mod paes;
pub(crate) mod parallel_eval;
pub mod particle_swarm;
pub mod random_search;
+pub mod rvea;
pub mod simulated_annealing;
pub mod sms_emoa;
pub mod spea2;
@@ -35,6 +36,7 @@ pub use nsga3::*;
pub use paes::*;
pub use particle_swarm::*;
pub use random_search::*;
+pub use rvea::*;
pub use simulated_annealing::*;
pub use sms_emoa::*;
pub use spea2::*;
diff --git a/src/algorithms/rvea.rs b/src/algorithms/rvea.rs
new file mode 100644
index 0000000..7a1b821
--- /dev/null
+++ b/src/algorithms/rvea.rs
@@ -0,0 +1,328 @@
+//! `Rvea` — Cheng, Jin, Olhofer & Sendhoff 2016 Reference Vector-guided EA.
+
+use rand::Rng as _;
+
+use crate::algorithms::parallel_eval::evaluate_batch;
+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_from_seed;
+use crate::pareto::front::{best_candidate, pareto_front};
+use crate::pareto::reference_points::das_dennis;
+use crate::traits::{Initializer, Optimizer, Variation};
+
+/// Configuration for [`Rvea`].
+#[derive(Debug, Clone)]
+pub struct RveaConfig {
+ /// Constant population size.
+ pub population_size: usize,
+ /// Number of generations.
+ pub generations: usize,
+ /// Number of divisions `H` for Das–Dennis reference vectors. Pop size
+ /// should be roughly `binomial(H + M − 1, M − 1)`.
+ pub reference_divisions: usize,
+ /// Penalty exponent `α`. The paper recommends 2.0.
+ pub alpha: f64,
+ /// Seed for the deterministic RNG.
+ pub seed: u64,
+}
+
+impl Default for RveaConfig {
+ fn default() -> Self {
+ Self {
+ population_size: 100,
+ generations: 250,
+ reference_divisions: 12,
+ alpha: 2.0,
+ seed: 42,
+ }
+ }
+}
+
+/// Reference Vector-guided Evolutionary Algorithm.
+#[derive(Debug, Clone)]
+pub struct Rvea {
+ /// Algorithm configuration.
+ pub config: RveaConfig,
+ /// Initial-decision sampler.
+ pub initializer: I,
+ /// Offspring-producing variation operator.
+ pub variation: V,
+}
+
+impl Rvea {
+ /// Construct an `Rvea`.
+ pub fn new(config: RveaConfig, initializer: I, variation: V) -> Self {
+ Self { config, initializer, variation }
+ }
+}
+
+impl
Optimizer
for Rvea
+where
+ P: Problem + Sync,
+ P::Decision: Send,
+ I: Initializer,
+ V: Variation,
+{
+ fn run(&mut self, problem: &P) -> OptimizationResult {
+ assert!(self.config.population_size > 0, "Rvea population_size must be > 0");
+ let n = self.config.population_size;
+ let objectives = problem.objectives();
+ let m = objectives.len();
+ // Reference vectors normalized to unit norm.
+ let raw_refs = das_dennis(m, self.config.reference_divisions);
+ let references: Vec> = raw_refs.into_iter().map(unit_normalize).collect();
+ assert!(!references.is_empty(), "Rvea: no reference vectors generated");
+
+ // Smallest angle between any two reference vectors — used to scale
+ // the APD penalty term.
+ let theta_max = smallest_neighbor_angle(&references);
+ let mut rng = rng_from_seed(self.config.seed);
+
+ let initial_decisions = self.initializer.initialize(n, &mut rng);
+ let mut population: Vec> =
+ evaluate_batch(problem, initial_decisions);
+ let mut evaluations = population.len();
+
+ for gen_idx in 0..self.config.generations {
+ // Phase 1: random parent selection + variation.
+ let mut offspring_decisions: Vec = Vec::with_capacity(n);
+ while offspring_decisions.len() < n {
+ let p1 = rng.random_range(0..population.len());
+ let p2 = rng.random_range(0..population.len());
+ let parents =
+ vec![population[p1].decision.clone(), population[p2].decision.clone()];
+ let children = self.variation.vary(&parents, &mut rng);
+ assert!(!children.is_empty(), "Rvea variation returned no children");
+ for child in children {
+ if offspring_decisions.len() >= n {
+ break;
+ }
+ offspring_decisions.push(child);
+ }
+ }
+ let offspring = evaluate_batch(problem, offspring_decisions);
+ evaluations += offspring.len();
+
+ // Combine + APD-based survival.
+ let mut combined: Vec> = Vec::with_capacity(2 * n);
+ combined.extend(population);
+ combined.extend(offspring);
+
+ // Ideal point z*.
+ let m_dim = m;
+ let mut ideal = vec![f64::INFINITY; m_dim];
+ for c in &combined {
+ let oriented = objectives.as_minimization(&c.evaluation.objectives);
+ for (k, v) in oriented.iter().enumerate() {
+ if *v < ideal[k] {
+ ideal[k] = *v;
+ }
+ }
+ }
+ // Translate.
+ let translated: Vec> = combined
+ .iter()
+ .map(|c| {
+ let oriented = objectives.as_minimization(&c.evaluation.objectives);
+ oriented.iter().enumerate().map(|(k, v)| v - ideal[k]).collect()
+ })
+ .collect();
+
+ // Associate each member with its closest-angle reference vector.
+ let mut assoc: Vec = vec![0; combined.len()];
+ let mut angles: Vec = vec![0.0; combined.len()];
+ for (i, t) in translated.iter().enumerate() {
+ let (best_ref, best_angle) = closest_reference(t, &references);
+ assoc[i] = best_ref;
+ angles[i] = best_angle;
+ }
+
+ // For each occupied reference vector, keep the member with the
+ // smallest APD score.
+ let alpha_t = (gen_idx as f64 / (self.config.generations as f64).max(1.0))
+ .powf(self.config.alpha);
+ let mut keep: Vec