pub mod configuration;
use crate::configuration::GaConfiguration;
use crate::error::GaError;
use crate::multi_objective::indicators::hypervolume;
use crate::multi_objective::pareto::{ParetoFront, ParetoIndividual};
use crate::observer::SmsEmoaObserver;
use crate::operations::{crossover, mutation};
use crate::sms_emoa::configuration::SmsEmoaConfiguration;
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 SmsEmoaGa<U>
where
U: LinearChromosome,
{
pub sms_config: SmsEmoaConfiguration,
pub ga_config: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub observer: Option<Arc<dyn SmsEmoaObserver<U> + Send + Sync>>,
}
impl<U> SmsEmoaGa<U>
where
U: LinearChromosome,
{
pub fn new(sms_config: SmsEmoaConfiguration, ga_config: GaConfiguration) -> Self {
SmsEmoaGa {
sms_config,
ga_config,
alleles: Vec::new(),
initialization_fn: None,
observer: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn SmsEmoaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
pub(crate) fn notify<F: FnOnce(&dyn SmsEmoaObserver<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.sms_config.num_objectives < 2 {
return Err(GaError::InvalidSmsEmoaConfiguration(
"num_objectives must be >= 2 for hypervolume-based selection".to_string(),
));
}
if self.sms_config.population_size < 2 {
return Err(GaError::InvalidSmsEmoaConfiguration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidSmsEmoaConfiguration(
"initialization_fn is required".to_string(),
));
}
if !self.sms_config.objective_directions.is_empty()
&& self.sms_config.objective_directions.len() != self.sms_config.num_objectives
{
return Err(GaError::InvalidSmsEmoaConfiguration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.sms_config.objective_directions.len(),
self.sms_config.num_objectives
)));
}
Ok(())
}
fn compute_hypervolume_contributions(
points: &[ParetoIndividual<U>],
reference_point: &[f64],
) -> Result<Vec<f64>, GaError> {
let n = points.len();
if n == 1 {
return Ok(vec![hypervolume(
&[points[0].objectives.clone()],
reference_point,
)?]);
}
let obj_slices: Vec<Vec<f64>> = points.iter().map(|p| p.objectives.clone()).collect();
let total_hv = hypervolume(&obj_slices, reference_point)?;
let mut contributions = Vec::with_capacity(n);
for i in 0..n {
let without: Vec<Vec<f64>> = obj_slices
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, v)| v.clone())
.collect();
let hv_without = hypervolume(&without, reference_point)?;
contributions.push(total_hv - hv_without);
}
Ok(contributions)
}
}
impl<U> SmsEmoaGa<U>
where
U: LinearChromosome
+ mutation::ValueMutable
+ VectorFitness
+ crate::traits::RealValuedMutation,
{
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.sms_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::InvalidSmsEmoaConfiguration(
"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_one_offspring(&self, population: &[ParetoIndividual<U>]) -> Result<U, GaError> {
let mut rng = crate::rng::make_rng();
let n = population.len();
let p1_idx = rng.random_range(0..n);
let p2_idx = rng.random_range(0..n);
let parent_a = &population[p1_idx].chromosome;
let parent_b = &population[p2_idx].chromosome;
let crossover_config = self.ga_config.crossover_configuration;
let crossover_prob = crossover_config.probability_max.unwrap_or(1.0);
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()]
};
let mutation_config = &self.ga_config.mutation_configuration;
let mut_prob = mutation_config.probability_max.unwrap_or(0.1);
for child in children.iter_mut() {
let mp: f64 = rng.random();
if mp <= mut_prob {
mutation_config
.method
.mutate(child, &mutation_config.method)?;
}
}
Ok(children
.into_iter()
.next()
.unwrap_or_else(|| parent_a.clone()))
}
pub fn run(&mut self) -> Result<ParetoFront<U>, GaError> {
self.validate()?;
crate::rng::set_seed(self.ga_config.rng_seed);
let max_gens = self.sms_config.max_generations;
let directions = self.sms_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.sms_config.num_objectives {
return Err(GaError::InvalidSmsEmoaConfiguration(format!(
"Expected {} objectives from fitness_values(), got {}",
self.sms_config.num_objectives, got
)));
}
}
let reference_point: Vec<f64> =
if let Some(ref rp) = self.sms_config.hypervolume_reference_point {
rp.clone()
} else {
let mut ref_pt = vec![f64::NEG_INFINITY; self.sms_config.num_objectives];
for ind in &population {
for (j, &val) in ind.objectives.iter().enumerate() {
if val > ref_pt[j] {
ref_pt[j] = val;
}
}
}
for val in ref_pt.iter_mut().take(self.sms_config.num_objectives) {
*val += 1.0;
}
ref_pt
};
for gen in 0..max_gens {
let offspring_chrom = self.create_one_offspring(&population)?;
let mut offspring_chrom = offspring_chrom;
offspring_chrom.calculate_fitness();
let offspring_obj = offspring_chrom.fitness_values().to_vec();
let offspring = ParetoIndividual::new(offspring_chrom, offspring_obj);
population.push(offspring);
let obj_slices: Vec<&[f64]> = population
.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; population.len()];
crate::multi_objective::non_dominated_sort::assign_ranks(&mut ranks, &fronts);
for (i, &r) in ranks.iter().enumerate() {
population[i].rank = r;
}
let max_rank = *ranks.iter().max().unwrap_or(&0);
let worst_front_indices: Vec<usize> = (0..population.len())
.filter(|&i| ranks[i] == max_rank)
.collect();
let t_hvc: Option<Instant> = if self.observer.is_some() {
#[cfg(not(target_arch = "wasm32"))]
{
Some(Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
} else {
None
};
let worst_front_individuals: Vec<&ParetoIndividual<U>> = worst_front_indices
.iter()
.map(|&i| &population[i])
.collect();
let hv_contributions = Self::compute_hypervolume_contributions(
&worst_front_individuals
.into_iter()
.cloned()
.collect::<Vec<_>>(),
&reference_point,
)?;
if let Some(start) = t_hvc {
self.notify(|obs| {
obs.on_hypervolume_contribution_assigned(
gen,
start.elapsed().as_secs_f64() * 1000.0,
worst_front_indices.len(),
)
});
}
let min_idx = hv_contributions
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let remove_pop_idx = worst_front_indices[min_idx];
population.remove(remove_pop_idx);
self.notify(|obs| obs.on_steady_state_removal(gen, population.len()));
}
let obj_slices: Vec<&[f64]> = population
.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; population.len()];
crate::multi_objective::non_dominated_sort::assign_ranks(&mut ranks, &fronts);
for (i, &r) in ranks.iter().enumerate() {
population[i].rank = r;
}
let front_individuals: Vec<ParetoIndividual<U>> =
population.into_iter().filter(|ind| ind.rank == 0).collect();
Ok(ParetoFront::new(front_individuals))
}
}