pub mod configuration;
use crate::configuration::GaConfiguration;
use crate::error::GaError;
use crate::ibea::configuration::IbeaConfiguration;
use crate::multi_objective::pareto::{ParetoFront, ParetoIndividual};
use crate::nsga2::configuration::ObjectiveDirection;
use crate::observer::IbeaObserver;
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 IbeaGa<U>
where
U: LinearChromosome,
{
pub ibea_config: IbeaConfiguration,
pub ga_config: GaConfiguration,
pub alleles: Vec<U::Gene>,
pub initialization_fn: Option<Arc<InitializationFn<U::Gene>>>,
pub observer: Option<Arc<dyn IbeaObserver<U> + Send + Sync>>,
}
impl<U> IbeaGa<U>
where
U: LinearChromosome,
{
pub fn new(ibea_config: IbeaConfiguration, ga_config: GaConfiguration) -> Self {
IbeaGa {
ibea_config,
ga_config,
alleles: Vec::new(),
initialization_fn: None,
observer: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn IbeaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
pub(crate) fn notify<F: FnOnce(&dyn IbeaObserver<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.ibea_config.num_objectives < 2 {
return Err(GaError::InvalidIbeaConfiguration(
"num_objectives must be >= 2 for indicator-based comparison".to_string(),
));
}
if self.ibea_config.population_size < 2 {
return Err(GaError::InvalidIbeaConfiguration(
"population_size must be >= 2".to_string(),
));
}
if self.initialization_fn.is_none() {
return Err(GaError::InvalidIbeaConfiguration(
"initialization_fn is required".to_string(),
));
}
if !self.ibea_config.objective_directions.is_empty()
&& self.ibea_config.objective_directions.len() != self.ibea_config.num_objectives
{
return Err(GaError::InvalidIbeaConfiguration(format!(
"objective_directions length ({}) must match num_objectives ({})",
self.ibea_config.objective_directions.len(),
self.ibea_config.num_objectives
)));
}
Ok(())
}
fn i_eps_plus(a: &[f64], b: &[f64], directions: &[ObjectiveDirection]) -> f64 {
let mut max_eps = f64::NEG_INFINITY;
for (idx, (ai, bi)) in a.iter().zip(b.iter()).enumerate() {
let dir = directions
.get(idx)
.copied()
.unwrap_or(ObjectiveDirection::Minimize);
let delta = match dir {
ObjectiveDirection::Minimize => ai - bi,
ObjectiveDirection::Maximize => bi - ai,
};
if delta > max_eps {
max_eps = delta;
}
}
max_eps.max(0.0)
}
fn compute_indicator_fitness(
population: &[ParetoIndividual<U>],
directions: &[ObjectiveDirection],
) -> Vec<f64> {
let n = population.len();
if n == 0 {
return vec![];
}
let mut indicators = vec![vec![0.0f64; n]; n];
let mut max_abs = 0.0f64;
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let val = Self::i_eps_plus(
&population[i].objectives,
&population[j].objectives,
directions,
);
indicators[i][j] = val;
if val.abs() > max_abs {
max_abs = val.abs();
}
}
}
let c = if max_abs > 1e-12 { max_abs } else { 1.0 };
let mut fitness = vec![0.0f64; n];
for (i, fi) in fitness.iter_mut().enumerate() {
let mut sum = 0.0;
for (j, row) in indicators.iter().enumerate() {
if i == j {
continue;
}
sum += (-row[i] / c).exp();
}
*fi = -sum;
}
fitness
}
fn environmental_selection(
population: &mut Vec<ParetoIndividual<U>>,
target_size: usize,
directions: &[ObjectiveDirection],
) -> usize {
let mut total_removed = 0usize;
while population.len() > target_size {
let fitness = Self::compute_indicator_fitness(population, directions);
let min_idx = fitness
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
population.remove(min_idx);
total_removed += 1;
}
total_removed
}
}
impl<U> IbeaGa<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.ibea_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::InvalidIbeaConfiguration(
"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,
population: &[ParetoIndividual<U>],
) -> Result<Vec<ParetoIndividual<U>>, GaError> {
let pop_size = self.ibea_config.population_size;
let mut rng = crate::rng::make_rng();
let n = population.len();
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 = 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 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)
}
pub fn run(&mut self) -> Result<ParetoFront<U>, GaError> {
self.validate()?;
crate::rng::set_seed(self.ga_config.rng_seed);
let pop_size = self.ibea_config.population_size;
let max_gens = self.ibea_config.max_generations;
let directions = self.ibea_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.ibea_config.num_objectives {
return Err(GaError::InvalidIbeaConfiguration(format!(
"Expected {} objectives from fitness_values(), got {}",
self.ibea_config.num_objectives, got
)));
}
}
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 mut _fitness = Self::compute_indicator_fitness(&population, &directions);
if let Some(start) = t_fitness {
self.notify(|obs| {
obs.on_indicator_fitness_assigned(
gen,
start.elapsed().as_secs_f64() * 1000.0,
population.len(),
)
});
}
let orig_size = population.len();
let removed = Self::environmental_selection(&mut population, orig_size, &directions);
self.notify(|obs| obs.on_environmental_selection(gen, population.len(), removed));
if population.len() < 2 {
population = self.initialize_population()?;
continue;
}
let mut offspring = self.create_offspring(&population)?;
population.append(&mut offspring);
population.truncate(pop_size);
}
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))
}
}