pub mod configuration;
use crate::configuration::GaConfiguration;
use crate::error::GaError;
use crate::multi_objective::pareto::{ParetoFront, ParetoIndividual};
use crate::nsga2::configuration::ObjectiveDirection;
use crate::observer::Spea2Observer;
use crate::operations::{crossover, mutation};
use crate::spea2::configuration::Spea2Configuration;
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 Spea2Ga<U>
where
U: LinearChromosome,
{
pub spea2_config: Spea2Configuration,
pub ga_config: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub observer: Option<Arc<dyn Spea2Observer<U> + Send + Sync>>,
}
impl<U> Spea2Ga<U>
where
U: LinearChromosome,
{
pub fn new(spea2_config: Spea2Configuration, ga_config: GaConfiguration) -> Self {
Spea2Ga {
spea2_config,
ga_config,
alleles: Vec::new(),
initialization_fn: None,
observer: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn Spea2Observer<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
pub(crate) fn notify<F: FnOnce(&dyn Spea2Observer<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.spea2_config.num_objectives == 0 {
return Err(GaError::InvalidSpea2Configuration(
"num_objectives must be > 0".to_string(),
));
}
if self.spea2_config.population_size < 2 {
return Err(GaError::InvalidSpea2Configuration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidSpea2Configuration(
"initialization_fn is required".to_string(),
));
}
if !self.spea2_config.objective_directions.is_empty()
&& self.spea2_config.objective_directions.len() != self.spea2_config.num_objectives
{
return Err(GaError::InvalidSpea2Configuration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.spea2_config.objective_directions.len(),
self.spea2_config.num_objectives
)));
}
if self.spea2_config.archive_size == 0 {
return Err(GaError::InvalidSpea2Configuration(
"archive_size must be > 0".to_string(),
));
}
if self.spea2_config.archive_size > self.spea2_config.population_size {
return Err(GaError::InvalidSpea2Configuration(format!(
"archive_size ({}) must not exceed population_size ({})",
self.spea2_config.archive_size, self.spea2_config.population_size
)));
}
Ok(())
}
fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
}
fn assign_spea2_fitness(
population: &[ParetoIndividual<U>],
archive: &[ParetoIndividual<U>],
directions: &[ObjectiveDirection],
) -> Vec<f64> {
let union: Vec<&ParetoIndividual<U>> = population.iter().chain(archive.iter()).collect();
let n = union.len();
let k = (n as f64).sqrt().floor() as usize;
let mut strength = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
if i != j
&& crate::multi_objective::pareto::dominates_with_directions(
&union[i].objectives,
&union[j].objectives,
directions,
)
{
strength[i] += 1.0;
}
}
}
let mut raw_fitness = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
if i != j
&& crate::multi_objective::pareto::dominates_with_directions(
&union[j].objectives,
&union[i].objectives,
directions,
)
{
raw_fitness[i] += strength[j];
}
}
}
let mut density = vec![0.0f64; n];
let effective_k = k.max(1); for i in 0..n {
let mut distances: Vec<f64> = (0..n)
.filter(|&j| j != i)
.map(|j| Self::euclidean_distance(&union[i].objectives, &union[j].objectives))
.collect();
distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let sigma_k = distances
.get(effective_k.saturating_sub(1))
.copied()
.unwrap_or(f64::MAX);
density[i] = 1.0 / (sigma_k + 2.0);
}
(0..n).map(|i| raw_fitness[i] + density[i]).collect()
}
fn truncate_archive(archive: &mut Vec<ParetoIndividual<U>>, target_size: usize) {
while archive.len() > target_size {
let n = archive.len();
let mut remove_idx = 0usize;
let mut remove_dist_list: Vec<f64> = Vec::new();
for i in 0..n {
let mut dists: Vec<f64> = (0..n)
.filter(|&j| j != i)
.map(|j| {
Self::euclidean_distance(&archive[i].objectives, &archive[j].objectives)
})
.collect();
dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
if i == 0 {
remove_dist_list = dists;
remove_idx = i;
} else {
let mut found_smaller = false;
for (a, b) in dists.iter().zip(remove_dist_list.iter()) {
if a < b {
found_smaller = true;
break;
} else if a > b {
break;
}
}
if found_smaller {
remove_dist_list = dists;
remove_idx = i;
}
}
}
archive.remove(remove_idx);
}
}
fn environmental_selection(
population: &[ParetoIndividual<U>],
archive: &[ParetoIndividual<U>],
fitness: &[f64],
target_archive_size: usize,
) -> Vec<ParetoIndividual<U>> {
let union: Vec<&ParetoIndividual<U>> = population.iter().chain(archive.iter()).collect();
let mut new_archive: Vec<ParetoIndividual<U>> = union
.iter()
.enumerate()
.filter(|(i, _)| fitness[*i] < 1.0)
.map(|(_, ind)| (*ind).clone())
.collect();
if new_archive.len() < target_archive_size {
let mut dominated: Vec<(f64, &ParetoIndividual<U>)> = union
.iter()
.enumerate()
.filter(|(i, _)| fitness[*i] >= 1.0)
.map(|(i, ind)| (fitness[i], *ind))
.collect();
dominated.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let needed = target_archive_size - new_archive.len();
for (_, ind) in dominated.into_iter().take(needed) {
new_archive.push(ind.clone());
}
} else if new_archive.len() > target_archive_size {
Self::truncate_archive(&mut new_archive, target_archive_size);
}
new_archive
}
fn binary_tournament_from_archive(
archive: &[ParetoIndividual<U>],
population: &[ParetoIndividual<U>],
rng: &mut impl rand::Rng,
) -> usize {
let pool = if archive.len() >= 2 {
archive
} else {
population
};
let n = pool.len();
let i = rng.random_range(0..n);
let j = rng.random_range(0..n);
let fi = pool[i].crowding_distance;
let fj = pool[j].crowding_distance;
if fi < fj {
i
} else if fj < fi {
j
} else if rng.random::<bool>() {
i
} else {
j
}
}
}
impl<U> Spea2Ga<U>
where
U: LinearChromosome
+ mutation::ValueMutable
+ VectorFitness
+ crate::traits::RealValuedMutation,
{
pub fn run(&mut self) -> Result<ParetoFront<U>, GaError> {
self.validate()?;
crate::rng::set_seed(self.ga_config.rng_seed);
let archive_size = self.spea2_config.archive_size;
let max_gens = self.spea2_config.max_generations;
let directions = self.spea2_config.effective_directions();
let mut population = self.initialize_population()?;
if let Some(first) = population.first() {
let got = first.chromosome.fitness_values().len();
if got != self.spea2_config.num_objectives {
return Err(GaError::InvalidSpea2Configuration(format!(
"Expected {} objectives from fitness_values(), got {}",
self.spea2_config.num_objectives, got
)));
}
}
let mut archive: Vec<ParetoIndividual<U>> = Vec::with_capacity(archive_size);
for gen in 0..max_gens {
let t_fitness: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let fitness = Self::assign_spea2_fitness(&population, &archive, &directions);
if let Some(start) = t_fitness {
self.notify(|obs| {
obs.on_fitness_assigned(
gen,
start.elapsed().as_secs_f64() * 1000.0,
population.len(),
archive.len(),
)
});
}
archive = Self::environmental_selection(&population, &archive, &fitness, archive_size);
let nd_count = {
let mut nd = 0usize;
for i in 0..archive.len() {
let mut dominated = false;
for j in 0..archive.len() {
if j != i
&& crate::multi_objective::pareto::dominates_with_directions(
&archive[j].objectives,
&archive[i].objectives,
&directions,
)
{
dominated = true;
break;
}
}
if !dominated {
nd += 1;
}
}
nd
};
self.notify(|obs| obs.on_archive_updated(gen, archive.len(), nd_count));
let archive_fitness = Self::assign_spea2_fitness(&archive, &[], &directions);
for (i, ind) in archive.iter_mut().enumerate() {
ind.crowding_distance = archive_fitness[i];
}
population = self.create_offspring(&archive)?;
}
let obj_slices: Vec<&[f64]> = archive
.iter()
.map(|ind| ind.objectives.as_slice())
.collect();
let fronts = crate::multi_objective::non_dominated_sort::non_dominated_sort_with_directions(
&obj_slices,
&directions,
);
let mut ranks = vec![0usize; archive.len()];
crate::multi_objective::non_dominated_sort::assign_ranks(&mut ranks, &fronts);
for (i, &r) in ranks.iter().enumerate() {
archive[i].rank = r;
}
let front_individuals: Vec<ParetoIndividual<U>> =
archive.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.spea2_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::InvalidSpea2Configuration(
"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(
&self,
archive: &[ParetoIndividual<U>],
) -> Result<Vec<ParetoIndividual<U>>, GaError> {
let pop_size = self.spea2_config.population_size;
let mut rng = crate::rng::make_rng();
let population: Vec<ParetoIndividual<U>> = Vec::new();
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 mut offspring: Vec<U> = Vec::with_capacity(pop_size);
let pairs_needed = (pop_size + 1).div_ceil(2);
for _ in 0..pairs_needed {
let p1_idx = Self::binary_tournament_from_archive(archive, &population, &mut rng);
let p2_idx = Self::binary_tournament_from_archive(archive, &population, &mut rng);
let parent_a = &archive[p1_idx].chromosome;
let parent_b = &archive[p2_idx].chromosome;
let p: f64 = rng.random();
let children = if p <= crossover_prob {
crossover::factory(parent_a, parent_b, crossover_config)?
} else {
vec![parent_a.clone(), parent_b.clone()]
};
for mut child in children {
let mp: f64 = rng.random();
if mp <= mut_prob {
mutation_config
.method
.mutate(&mut child, &mutation_config.method)?;
}
offspring.push(child);
if offspring.len() >= pop_size {
break;
}
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
let evaluated: Vec<ParetoIndividual<U>> = offspring
.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 evaluated: Vec<ParetoIndividual<U>> = offspring
.into_iter()
.map(|mut chrom| {
chrom.calculate_fitness();
let objectives = chrom.fitness_values().to_vec();
ParetoIndividual::new(chrom, objectives)
})
.collect();
Ok(evaluated)
}
}