pub mod configuration;
use crate::configuration::GaConfiguration;
use crate::error::GaError;
use crate::moead::configuration::{MoeaDConfiguration, ScalarizationFn};
use crate::multi_objective::non_dominated_sort::{
assign_ranks, non_dominated_sort_with_directions,
};
use crate::multi_objective::pareto::{ParetoFront, ParetoIndividual};
use crate::observer::MoeaDObserver;
use crate::operations::{crossover, mutation};
use crate::traits::{InitializationFn, LinearChromosome, MutationOperator, VectorFitness};
use rand::Rng;
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
use rayon::prelude::*;
use std::sync::Arc;
use std::time::Instant;
pub struct MoeaDGa<U>
where
U: LinearChromosome + VectorFitness,
{
pub moead_config: MoeaDConfiguration,
pub ga_config: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub observer: Option<Arc<dyn MoeaDObserver<U> + Send + Sync>>,
}
impl<U> MoeaDGa<U>
where
U: LinearChromosome + VectorFitness,
{
pub fn new(moead_config: MoeaDConfiguration, ga_config: GaConfiguration) -> Self {
MoeaDGa {
moead_config,
ga_config,
alleles: Vec::new(),
initialization_fn: None,
observer: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn MoeaDObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
pub(crate) fn notify<F: FnOnce(&dyn MoeaDObserver<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
pub fn with_alleles(mut self, alleles: Vec<U::Gene>) -> Self {
self.alleles = alleles;
self
}
pub fn with_initialization_fn<F>(mut self, f: F) -> Self
where
F: Fn(usize, Option<&[U::Gene]>) -> Vec<U::Gene> + Send + Sync + 'static,
{
self.initialization_fn = Some(Arc::new(f));
self
}
pub fn build(self) -> Result<Self, GaError> {
self.validate()?;
Ok(self)
}
pub fn validate(&self) -> Result<(), GaError> {
if self.moead_config.num_objectives == 0 {
return Err(GaError::InvalidMoeaDConfiguration(
"num_objectives must be > 0".to_string(),
));
}
if self.moead_config.population_size < 2 {
return Err(GaError::InvalidMoeaDConfiguration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidMoeaDConfiguration(
"initialization_fn is required".to_string(),
));
}
if !self.moead_config.objective_directions.is_empty()
&& self.moead_config.objective_directions.len() != self.moead_config.num_objectives
{
return Err(GaError::InvalidMoeaDConfiguration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.moead_config.objective_directions.len(),
self.moead_config.num_objectives
)));
}
if let Some(p) = self.moead_config.weight_vectors_auto_p() {
if p == 0 {
return Err(GaError::InvalidMoeaDConfiguration(
"Das-Dennis subdivision count p must be >= 1".to_string(),
));
}
}
let wvs = self.moead_config.effective_weight_vectors();
match wvs {
None => {
return Err(GaError::InvalidMoeaDConfiguration(
"weight vectors must be configured via with_weight_vectors_auto(p) or with_weight_vectors(vecs)".to_string(),
));
}
Some(vecs) => {
if vecs.is_empty() {
return Err(GaError::InvalidMoeaDConfiguration(
"weight vector list must not be empty".to_string(),
));
}
for (i, wv) in vecs.iter().enumerate() {
if wv.len() != self.moead_config.num_objectives {
return Err(GaError::InvalidMoeaDConfiguration(format!(
"weight vector {} has dimension {}, expected {}",
i,
wv.len(),
self.moead_config.num_objectives
)));
}
}
}
}
Ok(())
}
pub(crate) fn validate_and_get_weight_vectors(&self) -> Result<Vec<Vec<f64>>, GaError> {
if self.moead_config.num_objectives == 0 {
return Err(GaError::InvalidMoeaDConfiguration(
"num_objectives must be > 0".to_string(),
));
}
if self.moead_config.population_size < 2 {
return Err(GaError::InvalidMoeaDConfiguration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidMoeaDConfiguration(
"initialization_fn is required".to_string(),
));
}
if !self.moead_config.objective_directions.is_empty()
&& self.moead_config.objective_directions.len() != self.moead_config.num_objectives
{
return Err(GaError::InvalidMoeaDConfiguration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.moead_config.objective_directions.len(),
self.moead_config.num_objectives
)));
}
if let Some(p) = self.moead_config.weight_vectors_auto_p() {
if p == 0 {
return Err(GaError::InvalidMoeaDConfiguration(
"Das-Dennis subdivision count p must be >= 1".to_string(),
));
}
}
let vecs = self
.moead_config
.effective_weight_vectors()
.ok_or_else(|| {
GaError::InvalidMoeaDConfiguration(
"weight vectors must be configured via with_weight_vectors_auto(p) or with_weight_vectors(vecs)".to_string(),
)
})?;
if vecs.is_empty() {
return Err(GaError::InvalidMoeaDConfiguration(
"weight vector list must not be empty".to_string(),
));
}
for (i, wv) in vecs.iter().enumerate() {
if wv.len() != self.moead_config.num_objectives {
return Err(GaError::InvalidMoeaDConfiguration(format!(
"weight vector {} has dimension {}, expected {}",
i,
wv.len(),
self.moead_config.num_objectives
)));
}
}
Ok(vecs)
}
}
impl<U> MoeaDGa<U>
where
U: LinearChromosome
+ VectorFitness
+ mutation::ValueMutable
+ crate::traits::RealValuedMutation,
{
pub fn run(&mut self) -> Result<ParetoFront<U>, GaError> {
let weight_vectors = self.validate_and_get_weight_vectors()?;
crate::rng::set_seed(self.ga_config.rng_seed);
let pop_size = self.moead_config.population_size;
let max_gens = self.moead_config.max_generations;
let directions = self.moead_config.effective_directions();
let scalarization = self.moead_config.scalarization;
let num_objectives = self.moead_config.num_objectives;
let max_replacements = self.moead_config.max_neighbor_replacements;
let t_neigh = self
.moead_config
.neighborhood_size
.min(weight_vectors.len())
.max(1);
let neighbourhoods = precompute_neighbourhoods(&weight_vectors, t_neigh);
let mut population = self.initialize_population()?;
if let Some(first) = population.first() {
let got = first.chromosome.fitness_values().len();
if got != self.moead_config.num_objectives {
return Err(GaError::InvalidMoeaDConfiguration(format!(
"Expected {} objectives from fitness_values(), got {}",
self.moead_config.num_objectives, got
)));
}
}
let mut ideal_point = vec![f64::INFINITY; num_objectives];
for ind in &population {
for (k, &f) in ind.objectives.iter().enumerate() {
if f < ideal_point[k] {
ideal_point[k] = f;
}
}
}
for v in ideal_point.iter_mut() {
if !v.is_finite() {
*v = 0.0;
}
}
let n_subproblems = population.len();
for gen in 0..max_gens {
let t_sort: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let mut rng = crate::rng::make_rng();
for i in 0..n_subproblems {
let neigh = &neighbourhoods[i];
let parent_a_idx = neigh[rng.random_range(0..neigh.len())];
let parent_b_idx = neigh[rng.random_range(0..neigh.len())];
let offspring_chrom = self.create_offspring_for_subproblem(
&population[parent_a_idx].chromosome,
&population[parent_b_idx].chromosome,
&mut rng,
)?;
let mut offspring_chrom = offspring_chrom;
offspring_chrom.calculate_fitness();
let offspring_objectives = offspring_chrom.fitness_values().to_vec();
for (k, &f) in offspring_objectives.iter().enumerate() {
if f < ideal_point[k] {
ideal_point[k] = f;
}
}
let mut replacement_count = 0usize;
for &j in neigh {
if replacement_count >= max_replacements {
break;
}
let g_offspring = scalarize(
&offspring_objectives,
&weight_vectors[i],
&ideal_point,
scalarization,
);
let g_current = scalarize(
&population[j].objectives,
&weight_vectors[j],
&ideal_point,
scalarization,
);
if g_offspring < g_current {
population[j] = ParetoIndividual::new(
offspring_chrom.clone(),
offspring_objectives.clone(),
);
replacement_count += 1;
}
}
}
let obj_slices: Vec<&[f64]> = population
.iter()
.map(|ind| ind.objectives.as_slice())
.collect();
let fronts = non_dominated_sort_with_directions(&obj_slices, &directions);
let mut ranks = vec![0usize; population.len()];
assign_ranks(&mut ranks, &fronts);
for (i, &r) in ranks.iter().enumerate() {
population[i].rank = r;
}
let front_count = fronts.len();
if let Some(start) = t_sort {
self.notify(|obs| {
obs.on_non_dominated_sort_complete(gen, start.elapsed().as_secs_f64() * 1000.0)
});
}
self.notify(|obs| obs.on_pareto_front_assigned(gen, front_count, population.len()));
let _ = pop_size;
}
let front_individuals: Vec<ParetoIndividual<U>> =
population.into_iter().filter(|ind| ind.rank == 0).collect();
Ok(ParetoFront::new(front_individuals))
}
fn initialize_population(&self) -> Result<Vec<ParetoIndividual<U>>, GaError> {
let init_fn = self.initialization_fn.as_ref().ok_or_else(|| {
GaError::InitializationError("No initialization function set".to_string())
})?;
let pop_size = self.moead_config.population_size;
let genes_per_chrom = match self.ga_config.limit_configuration.chromosome_length {
crate::chromosomes::ChromosomeLength::Fixed(n) => n,
crate::chromosomes::ChromosomeLength::Variable { .. } => {
return Err(GaError::InvalidMoeaDConfiguration(
"ChromosomeLength::Variable is not yet supported (Phase 52). Use ChromosomeLength::Fixed.".into(),
));
}
};
let alleles = if self.alleles.is_empty() {
None
} else {
Some(self.alleles.as_slice())
};
let chromosomes: Vec<U> = crate::traits::initialize_chromosomes(
pop_size,
genes_per_chrom,
alleles,
init_fn,
None,
0,
);
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let population: Vec<ParetoIndividual<U>> = chromosomes
.into_par_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
#[cfg(any(target_arch = "wasm32", not(feature = "parallel")))]
let population: Vec<ParetoIndividual<U>> = chromosomes
.into_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
Ok(population)
}
fn create_offspring_for_subproblem(
&self,
parent_a: &U,
parent_b: &U,
rng: &mut impl Rng,
) -> Result<U, GaError> {
let crossover_config = self.ga_config.crossover_configuration;
let mutation_config = self.ga_config.mutation_configuration;
let crossover_prob = crossover_config.probability_max.unwrap_or(1.0);
let mut_prob = mutation_config.probability_max.unwrap_or(0.1);
let p: f64 = rng.random();
let mut children = if p <= crossover_prob {
crossover::factory(parent_a, parent_b, crossover_config)?
} else {
vec![parent_a.clone(), parent_b.clone()]
};
let mut child = children.pop().unwrap_or_else(|| parent_a.clone());
drop(children);
let mp: f64 = rng.random();
if mp <= mut_prob {
if matches!(
mutation_config.method,
crate::operations::Mutation::Differential(..)
) {
return Err(GaError::MutationError(
"Differential mutation is not supported in MOEA/D; \
use Cauchy, LevyFlight, Polynomial, or a standard mutation method instead."
.to_string(),
));
}
mutation_config
.method
.mutate(&mut child, &mutation_config.method)?;
}
Ok(child)
}
}
fn precompute_neighbourhoods(weight_vectors: &[Vec<f64>], t: usize) -> Vec<Vec<usize>> {
let n = weight_vectors.len();
let t = t.min(n).max(1);
let mut neighbourhoods: Vec<Vec<usize>> = Vec::with_capacity(n);
for i in 0..n {
let mut dists: Vec<(usize, f64)> = (0..n)
.map(|j| {
let d_sq: f64 = weight_vectors[i]
.iter()
.zip(weight_vectors[j].iter())
.map(|(a, b)| (a - b).powi(2))
.sum();
(j, d_sq.sqrt())
})
.collect();
dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
neighbourhoods.push(dists.into_iter().take(t).map(|(j, _)| j).collect());
}
neighbourhoods
}
fn scalarize(
objectives: &[f64],
weights: &[f64],
ideal: &[f64],
scalarization: ScalarizationFn,
) -> f64 {
match scalarization {
ScalarizationFn::Tchebycheff => objectives
.iter()
.zip(weights.iter())
.zip(ideal.iter())
.map(|((f_i, w_i), z_i)| w_i * (f_i - z_i).abs())
.fold(f64::NEG_INFINITY, f64::max),
ScalarizationFn::Pbi { theta } => {
let w_norm_sq: f64 = weights.iter().map(|w| w * w).sum::<f64>().max(f64::EPSILON);
let w_norm = w_norm_sq.sqrt();
let d1: f64 = objectives
.iter()
.zip(ideal.iter())
.zip(weights.iter())
.map(|((f_i, z_i), w_i)| (f_i - z_i) * w_i)
.sum::<f64>()
/ w_norm;
let d2_sq: f64 = objectives
.iter()
.zip(ideal.iter())
.zip(weights.iter())
.map(|((f_i, z_i), w_i)| {
let diff = f_i - z_i;
let proj = d1 * w_i / w_norm;
(diff - proj).powi(2)
})
.sum();
d1.abs() + theta * d2_sq.sqrt()
}
}
}