~ruther/ctu-fee-eoa

e72ea86dfb9bdd3992b14612afcc948f73e1a423 — Rutherther 5 days ago ff86cab
fix: properly evaluate nsga population

The full population has to be evaluated at once,
offsprings cannot be evaluated individually.
M codes/eoa_lib/src/evolution.rs => codes/eoa_lib/src/evolution.rs +1 -1
@@ 229,7 229,7 @@ pub fn evolution_algorithm_best_candidate
            &mut current_evaluation,
            &iteration,
            &mut stats,
            &current_population,
            &evaluated_offsprings,
            &mut last_best_candidate,
            &better_than_stats
        );

M codes/eoa_lib/src/multi_objective_evolution.rs => codes/eoa_lib/src/multi_objective_evolution.rs +162 -2
@@ 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
    <TChromosome: Clone,
     TResult: Clone,
     const DParents: usize,
     TSelection: Selection<TChromosome, TResult>,
     TFitness: FitnessFunction<In = TChromosome, Out = TResult>,
     TPairing: Pairing<DParents, Chromosome = TChromosome, Out = TResult>,
     TCrossover: Crossover<DParents, Chromosome = TChromosome, Out = TResult>,
     TReplacement: Replacement<TChromosome, TResult>,
     TPerturbation: PerturbationOperator<Chromosome = TChromosome>>(
    initial_population: Population<TChromosome>,
    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<TResult>,
    // TODO: termination condition
    iterations: usize,
    rng: &mut dyn RngCore,
    mut evolutionary_strategy: impl FnMut(
        usize,
        &EvolutionStats<TChromosome, TResult>,
        &EvaluatedPopulation<TChromosome, TResult>,

        &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<EvolutionCandidate<TChromosome, TResult>>) -> bool,
) -> Result<EvolutionResult<TChromosome, TResult>, Box<dyn Error>> {
    let mut current_evaluation = 0;

    let mut last_best_candidate: Option<EvolutionCandidate<TChromosome, TResult>> = None;
    let mut stats: EvolutionStats<TChromosome, TResult> = EvolutionStats {
        best_candidates: vec![]
    };

    fn apply_new_eval<TChromosome: Clone, TResult: Clone>(
        current_evaluation: &mut usize,
        current_iteration: &usize,
        stats: &mut EvolutionStats<TChromosome, TResult>,
        population: &EvaluatedPopulation<TChromosome, TResult>,
        last_best_candidate: &mut Option<EvolutionCandidate<TChromosome, TResult>>,
        better_than_stats: &impl Fn(&TChromosome, &TResult, &Option<EvolutionCandidate<TChromosome, TResult>>) -> 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,
        &current_population,
        &mut last_best_candidate,
        &better_than_stats);

    for iteration in 1..=iterations {
        // Selection
        let parents = selection.select(parents_count, &current_population, better_than, rng).collect::<Vec<_>>();
        let parent_pairings = pairing.pair(&current_population, parents.into_iter());

        // Crossover
        let mut offsprings = crossover.crossover(&current_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,
            &current_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<const OBJECTIVES: usize,
              TChromosome: Clone,
              TResult: Clone + Debug + PartialEq + Default + Copy + PartialOrd + Into<f64>,


@@ 562,7 722,7 @@ pub fn nsga_2<const OBJECTIVES: usize,
    ),
    better_than_stats: impl Fn(&TChromosome, &NSGAEvaluation<TResult, OBJECTIVES>, &Option<EvolutionCandidate<TChromosome, NSGAEvaluation<TResult, OBJECTIVES>>>) -> bool,
) -> Result<EvolutionResult<TChromosome, [TResult; OBJECTIVES]>, Box<dyn Error>> {
    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<TResult, OBJECTIVES>, &Option<EvolutionCandidate<TChromosome, NSGAEvaluation<TResult, OBJECTIVES>>>) -> bool,
) -> Result<EvolutionResult<TChromosome, [TResult; OBJECTIVES]>, Box<dyn Error>> {
    let result = evolution_algorithm_best_candidate(
    let result = evolution_algorithm_best_candidate_modified(
        initial_population,
        parents_count,
        &mut ConstrainedNSGAFitness::new(objectives, constraints, better_than),

M codes/eoa_lib/src/population.rs => codes/eoa_lib/src/population.rs +33 -0
@@ 17,6 17,12 @@ pub struct EvaluatedPopulation<TChromosome, TResult> {
}

impl<TChromosome> Population<TChromosome> {
    pub fn empty() -> Self {
        Self {
            population: vec![]
        }
    }

    pub fn from_vec(vec: Vec<TChromosome>) -> Self {
        Self {
            population: vec


@@ 34,6 40,10 @@ impl<TChromosome> Population<TChromosome> {
        )
    }

    pub fn join(&mut self, mut offsprings: Population<TChromosome>) {
        self.population.append(&mut offsprings.population);
    }

    pub fn into_iter(self) -> impl Iterator<Item = TChromosome> {
        self.population.into_iter()
    }


@@ 54,6 64,12 @@ impl<TInput, TResult> EvaluatedChromosome<TInput, TResult> {
}

impl<TChromosome, TResult> EvaluatedPopulation<TChromosome, TResult> {
    pub fn empty() -> Self {
        Self {
            population: vec![]
        }
    }

    pub fn new() -> Self {
        Self {
            population: vec![]


@@ 82,6 98,16 @@ impl<TChromosome, TResult> EvaluatedPopulation<TChromosome, TResult> {
        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<TChromosome, TResult>) {
        self.population.push(c)
    }


@@ 90,6 116,13 @@ impl<TChromosome, TResult> EvaluatedPopulation<TChromosome, TResult> {
        self.population
    }

    pub fn nonevaluated(self) -> Population<TChromosome> {
        Population::from_vec(self.deconstruct()
                             .into_iter()
                             .map(|individual| individual.chromosome)
                             .collect::<Vec<_>>())
    }

    pub fn join(&mut self, mut offsprings: EvaluatedPopulation<TChromosome, TResult>) {
        self.population.append(&mut offsprings.population);
    }