~ruther/ctu-fee-eoa

458589fdc2ace8fd18ebc5f808ee3a697d4f24f8 — Rutherther 6 days ago 66b5f22
feat: implement constrained nsga
1 files changed, 289 insertions(+), 1 deletions(-)

M codes/eoa_lib/src/multi_objective_evolution.rs
M codes/eoa_lib/src/multi_objective_evolution.rs => codes/eoa_lib/src/multi_objective_evolution.rs +289 -1
@@ 1,9 1,11 @@
use std::convert::Infallible;
use std::ops::{AddAssign, Sub};
use std::{cmp::Ordering, error::Error};
use std::fmt::Debug;

use rand::RngCore;

use crate::constraints::ConstraintFunction;
use crate::evolution::{evolution_algorithm_best_candidate, EvolutionCandidate, EvolutionStats};
use crate::population::{EvaluatedChromosome, EvaluatedPopulation};
use crate::{comparison::{BetterThanOperator, MinimizingOperator}, crossover::Crossover, evolution::{evolution_algorithm, EvolutionResult}, fitness::FitnessFunction, pairing::Pairing, perturbation::PerturbationOperator, population::Population, replacement::BestReplacement, selection::TournamentSelection};


@@ 17,6 19,16 @@ pub struct MOFitness<'a,
    objectives: [Box<dyn FitnessFunction<In = TChromosome, Out = TOut, Err = TErr> + 'a>; OBJECTIVES]
}

pub struct ConstrainedMOFitness<'a,
                                const OBJECTIVES: usize,
                                const CONSTRAINTS: usize,
                                TChromosome,
                                TOut,
                                TErr: Error> {
    objectives: [Box<dyn FitnessFunction<In = TChromosome, Out = TOut, Err = TErr> + 'a>; OBJECTIVES],
    constraints: [Box<dyn ConstraintFunction<Chromosome = TChromosome, Out = TOut, Err = Infallible>>; CONSTRAINTS]
}

pub fn a_dominates_b<const OBJECTIVES: usize, TOut>(
    a: &[TOut; OBJECTIVES], b: &[TOut; OBJECTIVES],
    better_than: &(impl BetterThanOperator<TOut> + ?Sized)


@@ 24,6 36,31 @@ pub fn a_dominates_b<const OBJECTIVES: usize, TOut>(
    a.iter().zip(b.iter()).all(|(a, b)| better_than.better_than(a, b) || better_than.equal(a, b))
}

pub fn a_constraint_dominates_b<const OBJECTIVES: usize, const CONSTRAINTS: usize, TOut: PartialOrd>(
    a: &ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>, b: &ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>,
    better_than: &(impl BetterThanOperator<TOut> + ?Sized)
) -> bool {
    let a_feasible = a.is_feasible_full;
    let b_feasible = b.is_feasible_full;

    match (a_feasible, b_feasible) {
        (true, true) => a_dominates_b(&a.evaluations, &b.evaluations, better_than),
        (true, false) => true,
        (false, true) => false,
        (false, false) => {
            (0..CONSTRAINTS).all(|i| {
                if a.is_feasible[i] {
                    true
                } else if b.is_feasible[i] {
                    false
                } else {
                    a.constraints[i] < b.constraints[i]
                }
            })
        }
    }
}

pub fn a_dominated_by_b<const OBJECTIVES: usize, TOut>(
    a: &[TOut; OBJECTIVES],
    b: &[TOut; OBJECTIVES],


@@ 32,6 69,13 @@ pub fn a_dominated_by_b<const OBJECTIVES: usize, TOut>(
    a_dominates_b(b, a, better_than)
}

pub fn a_constraint_dominated_by_b<const OBJECTIVES: usize, const CONSTRAINTS: usize, TOut: PartialOrd>(
    a: &ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>, b: &ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>,
    better_than: &(impl BetterThanOperator<TOut> + ?Sized)
) -> bool {
    a_constraint_dominates_b(b, a, better_than)
}

impl<'a,
     const OBJECTIVES: usize,
     TChromosome,


@@ 65,6 109,69 @@ impl<'a,
    }
}

#[derive(Clone, PartialEq, Debug)]
pub struct ConstrainedMOEvaluation<const OBJECTIVES: usize, const CONSTRAINTS: usize, TOut> {
    evaluations: [TOut; OBJECTIVES],
    constraints: [TOut; CONSTRAINTS],
    is_feasible: [bool; CONSTRAINTS],
    is_feasible_full: bool,
}

impl<'a,
     const OBJECTIVES: usize,
     const CONSTRAINTS: usize,
     TChromosome,
     TOut,
     TErr: Error>
    ConstrainedMOFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr>
{
    pub fn new(
        objectives: [Box<dyn FitnessFunction<In = TChromosome, Out = TOut, Err = TErr> + 'a>; OBJECTIVES],
        constraints: [Box<dyn ConstraintFunction<Chromosome = TChromosome, Out = TOut, Err = Infallible>>; CONSTRAINTS]
    ) -> Self {
        Self { objectives, constraints }
    }
}

impl<'a,
     const OBJECTIVES: usize,
     const CONSTRAINTS: usize,
     TChromosome,
     TOut: Default + Copy,
     TErr: Error + 'static>
    FitnessFunction for ConstrainedMOFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr> {
    type In = TChromosome;
    type Out = ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>;
    type Err = TErr;

    fn fit(&self, inp: &Self::In) -> Result<Self::Out, Self::Err> {
        let mut evaluations = [Default::default(); OBJECTIVES];
        let mut constraints = [Default::default(); CONSTRAINTS];
        let mut is_feasible = [true; CONSTRAINTS];
        let mut is_feasible_full = true;

        for (i, objective) in self.objectives.iter().enumerate() {
            evaluations[i] = objective.fit(inp)?;
        }

        for (i, constraint) in self.constraints.iter().enumerate() {
            constraints[i] = constraint.evaluate(inp).unwrap();
            if !constraint.is_feasible(inp).unwrap() {
                is_feasible_full = false;
                is_feasible[i] = false;
            }
        }

        Ok(ConstrainedMOEvaluation {
            evaluations,
            constraints,
            is_feasible,
            is_feasible_full
        })
    }
}


#[derive(Debug, Clone, PartialEq)]
pub struct NSGAEvaluation<TOut: PartialEq + Clone, const OBJECTIVES: usize> {
    pub evaluations: [TOut; OBJECTIVES],


@@ 90,7 197,12 @@ pub struct NSGAFitness<'a, const OBJECTIVES: usize, TChromosome, TOut, TErr: Err
    better_than: &'a dyn BetterThanOperator<TOut>
}

pub fn get_nondominated_fronts<const OBJECTIVES: usize,TChromosome, TOut>(
pub struct ConstrainedNSGAFitness<'a, const OBJECTIVES: usize, const CONSTRAINTS: usize, TChromosome, TOut, TErr: Error> {
    mo_fitness: ConstrainedMOFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr>,
    better_than: &'a dyn BetterThanOperator<TOut>
}

pub fn get_nondominated_fronts<const OBJECTIVES: usize, TChromosome, TOut>(
    fitted: &[EvaluatedChromosome<TChromosome, [TOut; OBJECTIVES]>],
    better_than: &(impl BetterThanOperator<TOut> + ?Sized)
) -> (Vec<usize>, Vec<Vec<usize>>) {


@@ 144,6 256,60 @@ pub fn get_nondominated_fronts<const OBJECTIVES: usize,TChromosome, TOut>(
    (fronts, front_indices)
}

pub fn get_constrained_nondominated_fronts<const OBJECTIVES: usize, const CONSTRAINTS: usize, TChromosome, TOut: PartialOrd>(
    fitted: &[EvaluatedChromosome<TChromosome, ConstrainedMOEvaluation<OBJECTIVES, CONSTRAINTS, TOut>>],
    better_than: &(impl BetterThanOperator<TOut> + ?Sized)
) -> (Vec<usize>, Vec<Vec<usize>>) {
    let mut remaining_indices = (0..fitted.len()).collect::<Vec<_>>();
    let mut current_front = 0;
    let mut fronts = vec![0; fitted.len()];
    let mut front_indices = vec![];
    let mut current_for_removal = vec![];

    while !remaining_indices.is_empty() {
        let mut current_front_indices = vec![];
        'outer: for (i, &current) in remaining_indices.iter().enumerate() {
            let current_eval = &fitted[current].evaluation;
            for &i in remaining_indices.iter() {
                let i_eval = &fitted[i].evaluation;

                if i == current {
                    continue;
                }

                if current_eval.evaluations
                    .iter()
                    .zip(i_eval.evaluations.iter())
                    .all(|(a, b)| better_than.equal(a, b)) {
                    continue;
                }

                if a_constraint_dominated_by_b(current_eval, i_eval, better_than) {
                    // At least one dominates current...
                    continue 'outer;
                }
            }

            // None dominates current...
            fronts[current] = current_front;
            current_front_indices.push(current);
            current_for_removal.push(i);
        }

        front_indices.push(current_front_indices);

        for &for_removal in current_for_removal.iter().rev() {
            remaining_indices.swap_remove(for_removal);
        }
        current_for_removal.clear();

        current_front += 1;
    }

    // current_front is counting from backwards, so reverse it
    (fronts, front_indices)
}

pub fn get_front_crowding_distances<const NORMALIZE: bool,
                              const OBJECTIVES: usize,
                              TChromosome,


@@ 302,6 468,73 @@ impl<'a,
    }
}

impl<'a,
     const OBJECTIVES: usize,
     const CONSTRAINTS: usize,
     TChromosome,
     TOut: Copy + Into<f64>,
     TErr: Error>
    ConstrainedNSGAFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr> {
        pub fn new(
            objectives: [Box<dyn FitnessFunction<In = TChromosome, Out = TOut, Err = TErr> + 'a>; OBJECTIVES],
            constraints: [Box<dyn ConstraintFunction<Chromosome = TChromosome, Out = TOut, Err = Infallible>>; CONSTRAINTS],
            better_than: &'a impl BetterThanOperator<TOut>
        ) -> Self {
            Self {
                mo_fitness: ConstrainedMOFitness::<'a, OBJECTIVES, CONSTRAINTS, _, _, _>::new(objectives, constraints),
                better_than
            }
        }
}

impl<'a,
     const OBJECTIVES: usize,
     const CONSTRAINTS: usize,
     TChromosome: Clone,
     TOut: PartialEq + Default + Copy + PartialOrd + Into<f64>,
     TErr: Error + 'static>
    FitnessFunction for ConstrainedNSGAFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr> {
    type In = TChromosome;
    type Out = NSGAEvaluation<TOut, OBJECTIVES>;
    type Err = TErr;

    fn fit(&self, _: &Self::In) -> Result<Self::Out, Self::Err> {
        panic!("NSGA fitness does not implement single fit. To get all the non dominated fronts informations, fit_population needs to be called.");
    }

    fn fit_population(&self, inp: Vec<Self::In>) -> Result<Vec<crate::population::EvaluatedChromosome<Self::In, Self::Out>>, Self::Err> {
        let fitted = self.mo_fitness.fit_population(inp)?;

        let (fronts, front_indices) = get_constrained_nondominated_fronts(&fitted, self.better_than);

        let fitted =
            fitted.into_iter().map(|individual|
                                   EvaluatedChromosome {
                                       chromosome: individual.chromosome,
                                       evaluation: individual.evaluation.evaluations
                                   }).collect::<Vec<_>>();

        let cd = get_crowding_distances
            ::<true, OBJECTIVES, TChromosome, TOut>(&fitted, &front_indices, self.better_than);

        Ok(fitted
           .into_iter()
           .zip(fronts.into_iter())
           .zip(cd.into_iter())
           .map(|((individual, nondominated_front), crowding_distance)| {
            EvaluatedChromosome {
                chromosome: individual.chromosome,
                evaluation: NSGAEvaluation {
                    evaluations: individual.evaluation,
                    nondominated_front,
                    crowding_distance
                }
            }
           })
           .collect())
    }
}

pub fn nsga_2<const OBJECTIVES: usize,
              TChromosome: Clone,
              TResult: Clone + Debug + PartialEq + Default + Copy + PartialOrd + Into<f64>,


@@ 354,6 587,61 @@ pub fn nsga_2<const OBJECTIVES: usize,
    }))
}

pub fn constrained_nsga_2<
        const OBJECTIVES: usize,
        const CONSTRAINTS: usize,
        TChromosome: Clone,
        TResult: Clone + Debug + PartialEq + Default + Copy + PartialOrd + Into<f64>,
        TErr: Error + 'static,
        const DParents: usize,
        TPairing: Pairing<DParents, Chromosome = TChromosome, Out = NSGAEvaluation<TResult, OBJECTIVES>>,
        TCrossover: Crossover<DParents, Chromosome = TChromosome, Out = NSGAEvaluation<TResult, OBJECTIVES>>,
        TPerturbation: PerturbationOperator<Chromosome = TChromosome>> (
    initial_population: Population<TChromosome>,
    parents_count: usize,
    // TODO: possibility for objectives with different outputs?
    objectives: [Box<dyn FitnessFunction<In = TChromosome, Out = TResult, Err = TErr> + '_>; OBJECTIVES],
    constraints: [Box<dyn ConstraintFunction<Chromosome = TChromosome, Out = TResult, Err = Infallible>>; CONSTRAINTS],
    pairing: &mut TPairing,
    crossover: &mut TCrossover,
    perturbation: &mut TPerturbation,
    // TODO: possibility for better_than different for different fitness functions
    better_than: &impl BetterThanOperator<TResult>,
    // TODO: termination condition
    iterations: usize,
    rng: &mut dyn RngCore,
    mut evolutionary_strategy: impl FnMut(
        usize,
        &EvolutionStats<TChromosome, NSGAEvaluation<TResult, OBJECTIVES>>,
        &EvaluatedPopulation<TChromosome, NSGAEvaluation<TResult, OBJECTIVES>>

        // TODO
    ),
    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(
        initial_population,
        parents_count,
        &mut ConstrainedNSGAFitness::new(objectives, constraints, better_than),
        &mut TournamentSelection::new(5, 0.99),
        pairing,
        crossover,
        perturbation,
        &mut BestReplacement::new(),
        &MinimizingOperator::new(),
        iterations,
        rng,
        |iteration, stats, population, _, _, _, _, _, _| {
            evolutionary_strategy(iteration, stats, population);
        },
        better_than_stats
    )?;

    Ok(result.map(|res| {
        res.evaluations
    }))
}

#[cfg(test)]
pub mod test {
    use std::str::FromStr;