From e72ea86dfb9bdd3992b14612afcc948f73e1a423 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 6 Dec 2025 13:37:39 +0100 Subject: [PATCH] fix: properly evaluate nsga population The full population has to be evaluated at once, offsprings cannot be evaluated individually. --- codes/eoa_lib/src/evolution.rs | 2 +- .../eoa_lib/src/multi_objective_evolution.rs | 164 +++++++++++++++++- codes/eoa_lib/src/population.rs | 33 ++++ 3 files changed, 196 insertions(+), 3 deletions(-) diff --git a/codes/eoa_lib/src/evolution.rs b/codes/eoa_lib/src/evolution.rs index 9858b4a27795505bbb689dbdb34017968debe2c5..39e028c0fcad961d870d8c7ba116e308bec84431 100644 --- a/codes/eoa_lib/src/evolution.rs +++ b/codes/eoa_lib/src/evolution.rs @@ -229,7 +229,7 @@ pub fn evolution_algorithm_best_candidate &mut current_evaluation, &iteration, &mut stats, - ¤t_population, + &evaluated_offsprings, &mut last_best_candidate, &better_than_stats ); diff --git a/codes/eoa_lib/src/multi_objective_evolution.rs b/codes/eoa_lib/src/multi_objective_evolution.rs index 5017331d442a7fcf742fd373a4144a149d4d4e90..686adb849f40b3c7a8af90467a0d2d9cabfd1956 100644 --- a/codes/eoa_lib/src/multi_objective_evolution.rs +++ b/codes/eoa_lib/src/multi_objective_evolution.rs @@ -2,6 +2,8 @@ use std::convert::Infallible; use std::ops::{AddAssign, Sub}; use std::{cmp::Ordering, error::Error}; use std::fmt::Debug; +use crate::selection::Selection; +use crate::replacement::Replacement; use rand::RngCore; @@ -534,6 +536,164 @@ impl<'a, } } +// NOTE: this is a copy of evolution_algorithm from evolution.rs, +// this is mainly for lack of time to provide a more generalized implementeation. +// The problem with the original implementation is that it evaluates the offsprings +// without evaluating the parents. But for getting proper non-dominated front sorting, +// we need to evaluate joined population. +pub fn evolution_algorithm_best_candidate_modified + , + TFitness: FitnessFunction, + TPairing: Pairing, + TCrossover: Crossover, + TReplacement: Replacement, + TPerturbation: PerturbationOperator>( + initial_population: Population, + parents_count: usize, + fitness: &mut TFitness, + selection: &mut TSelection, + pairing: &mut TPairing, + crossover: &mut TCrossover, + perturbation: &mut TPerturbation, + replacement: &mut TReplacement, + better_than: &impl BetterThanOperator, + // TODO: termination condition + iterations: usize, + rng: &mut dyn RngCore, + mut evolutionary_strategy: impl FnMut( + usize, + &EvolutionStats, + &EvaluatedPopulation, + + &mut TFitness, + &mut TSelection, + &mut TPairing, + &mut TCrossover, + &mut TPerturbation, + &mut TReplacement + ), + // For the statistics, evaluate if a candidate is better. Potential for different functrion than better_than that's used + // for the replacement, selection etc. + better_than_stats: impl Fn(&TChromosome, &TResult, &Option>) -> bool, +) -> Result, Box> { + let mut current_evaluation = 0; + + let mut last_best_candidate: Option> = None; + let mut stats: EvolutionStats = EvolutionStats { + best_candidates: vec![] + }; + + fn apply_new_eval( + current_evaluation: &mut usize, + current_iteration: &usize, + stats: &mut EvolutionStats, + population: &EvaluatedPopulation, + last_best_candidate: &mut Option>, + better_than_stats: &impl Fn(&TChromosome, &TResult, &Option>) -> bool, + ) { + for individual in population.iter() { + let evaluation = &individual.evaluation; + let chromosome = &individual.chromosome; + + if better_than_stats(chromosome, evaluation, last_best_candidate) { + let previous_best = std::mem::replace( + last_best_candidate, + Some(EvolutionCandidate { + evaluated_chromosome: EvaluatedChromosome { + chromosome: chromosome.clone(), + evaluation: evaluation.clone(), + }, + evaluation: *current_evaluation, + iteration: *current_iteration + })); + + if let Some(previous_best) = previous_best { + stats.best_candidates.push(previous_best); + } + } + *current_evaluation += 1; + } + } + + let mut current_population = + initial_population.evaluate(fitness)?; + apply_new_eval( + &mut current_evaluation, + &0, + &mut stats, + ¤t_population, + &mut last_best_candidate, + &better_than_stats); + + for iteration in 1..=iterations { + // Selection + let parents = selection.select(parents_count, ¤t_population, better_than, rng).collect::>(); + let parent_pairings = pairing.pair(¤t_population, parents.into_iter()); + + // Crossover + let mut offsprings = crossover.crossover(¤t_population, parent_pairings, rng); + + // Mutation + for offspring in offsprings.iter_mut() { + perturbation.perturb(offspring, rng); + } + + // This is what was wrong. + // let evaluated_offsprings = + // offsprings.evaluate(fitness)?; + + let original_population_len = current_population.population.len(); + let reevaluated_joined = { + let mut curr = current_population.nonevaluated(); + curr.join(offsprings); + curr.evaluate(fitness)? + }; + + apply_new_eval( + &mut current_evaluation, + &iteration, + &mut stats, + &reevaluated_joined, + &mut last_best_candidate, + &better_than_stats + ); + + let (reevaluated_current_population, evaluated_offsprings) = + reevaluated_joined.split_at(original_population_len); + + // Replace + current_population = replacement.replace(reevaluated_current_population, evaluated_offsprings, better_than, rng); + + evolutionary_strategy( + iteration, + &stats, + ¤t_population, + fitness, + selection, + pairing, + crossover, + perturbation, + replacement + ); + } + + let best_candidate = last_best_candidate.as_ref().map(|x| x.evaluated_chromosome.clone()); + if last_best_candidate.is_some() { + stats.best_candidates.push(last_best_candidate.unwrap()); + } + + Ok(EvolutionResult { + population: current_population, + best_candidate, + stats, + iterations, + evaluations: current_evaluation + }) +} + pub fn nsga_2, @@ -562,7 +722,7 @@ pub fn nsga_2, &Option>>) -> bool, ) -> Result, Box> { - let result = evolution_algorithm_best_candidate( + let result = evolution_algorithm_best_candidate_modified( initial_population, parents_count, &mut NSGAFitness::new(objectives, better_than), @@ -616,7 +776,7 @@ pub fn constrained_nsga_2< ), better_than_stats: impl Fn(&TChromosome, &NSGAEvaluation, &Option>>) -> bool, ) -> Result, Box> { - let result = evolution_algorithm_best_candidate( + let result = evolution_algorithm_best_candidate_modified( initial_population, parents_count, &mut ConstrainedNSGAFitness::new(objectives, constraints, better_than), diff --git a/codes/eoa_lib/src/population.rs b/codes/eoa_lib/src/population.rs index 826140febdfe4fe0f7611e63d1e3659fb5f926ca..96cecdcda98d9acf30a463d9ddf9964b32489d2a 100644 --- a/codes/eoa_lib/src/population.rs +++ b/codes/eoa_lib/src/population.rs @@ -17,6 +17,12 @@ pub struct EvaluatedPopulation { } impl Population { + pub fn empty() -> Self { + Self { + population: vec![] + } + } + pub fn from_vec(vec: Vec) -> Self { Self { population: vec @@ -34,6 +40,10 @@ impl Population { ) } + pub fn join(&mut self, mut offsprings: Population) { + self.population.append(&mut offsprings.population); + } + pub fn into_iter(self) -> impl Iterator { self.population.into_iter() } @@ -54,6 +64,12 @@ impl EvaluatedChromosome { } impl EvaluatedPopulation { + pub fn empty() -> Self { + Self { + population: vec![] + } + } + pub fn new() -> Self { Self { population: vec![] @@ -82,6 +98,16 @@ impl EvaluatedPopulation { best_so_far } + pub fn split_at(self, len: usize) -> (Self, Self) { + let mut left = self.population; + let right = left.split_off(len); + + ( + Self::from_vec(left), + Self::from_vec(right), + ) + } + pub fn add(&mut self, c: EvaluatedChromosome) { self.population.push(c) } @@ -90,6 +116,13 @@ impl EvaluatedPopulation { self.population } + pub fn nonevaluated(self) -> Population { + Population::from_vec(self.deconstruct() + .into_iter() + .map(|individual| individual.chromosome) + .collect::>()) + } + pub fn join(&mut self, mut offsprings: EvaluatedPopulation) { self.population.append(&mut offsprings.population); }