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}; // Multi objective fitness function, returning a list of evaluations pub struct MOFitness<'a, const OBJECTIVES: usize, TChromosome, TOut, TErr: Error> { objectives: [Box + 'a>; OBJECTIVES] } pub struct ConstrainedMOFitness<'a, const OBJECTIVES: usize, const CONSTRAINTS: usize, TChromosome, TOut, TErr: Error> { objectives: [Box + 'a>; OBJECTIVES], constraints: [Box>; CONSTRAINTS] } pub fn a_dominates_b( a: &[TOut; OBJECTIVES], b: &[TOut; OBJECTIVES], better_than: &(impl BetterThanOperator + ?Sized) ) -> bool { 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( a: &ConstrainedMOEvaluation, b: &ConstrainedMOEvaluation, better_than: &(impl BetterThanOperator + ?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( a: &[TOut; OBJECTIVES], b: &[TOut; OBJECTIVES], better_than: &(impl BetterThanOperator + ?Sized) ) -> bool { a_dominates_b(b, a, better_than) } pub fn a_constraint_dominated_by_b( a: &ConstrainedMOEvaluation, b: &ConstrainedMOEvaluation, better_than: &(impl BetterThanOperator + ?Sized) ) -> bool { a_constraint_dominates_b(b, a, better_than) } impl<'a, const OBJECTIVES: usize, TChromosome, TOut, TErr: Error> MOFitness<'a, OBJECTIVES, TChromosome, TOut, TErr> { pub fn new(objectives: [Box + 'a>; OBJECTIVES]) -> Self { Self { objectives } } } impl<'a, const OBJECTIVES: usize, TChromosome, TOut: Default + Copy, TErr: Error + 'static> FitnessFunction for MOFitness<'a, OBJECTIVES, TChromosome, TOut, TErr> { type In = TChromosome; type Out = [TOut; OBJECTIVES]; type Err = TErr; fn fit(&self, inp: &Self::In) -> Result { let mut evaluations = [Default::default(); OBJECTIVES]; for (i, objective) in self.objectives.iter().enumerate() { evaluations[i] = objective.fit(inp)?; } Ok(evaluations) } } #[derive(Clone, PartialEq, Debug)] pub struct ConstrainedMOEvaluation { 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 + 'a>; OBJECTIVES], constraints: [Box>; 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; type Err = TErr; fn fit(&self, inp: &Self::In) -> Result { 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 { pub evaluations: [TOut; OBJECTIVES], pub nondominated_front: usize, pub crowding_distance: f64 } impl PartialOrd for NSGAEvaluation { fn partial_cmp(&self, other: &Self) -> Option { match self.nondominated_front.partial_cmp(&other.nondominated_front) { Some(core::cmp::Ordering::Equal) => {} ord => return ord, } other.crowding_distance.partial_cmp(&self.crowding_distance) } } pub struct NSGAFitness<'a, const OBJECTIVES: usize, TChromosome, TOut, TErr: Error> { mo_fitness: MOFitness<'a, OBJECTIVES, TChromosome, TOut, TErr>, better_than: &'a dyn BetterThanOperator } 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 } pub fn get_nondominated_fronts( fitted: &[EvaluatedChromosome], better_than: &(impl BetterThanOperator + ?Sized) ) -> (Vec, Vec>) { let mut remaining_indices = (0..fitted.len()).collect::>(); 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, ¤t) 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 .iter() .zip(i_eval.iter()) .all(|(a, b)| better_than.equal(a, b)) { continue; } if a_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_constrained_nondominated_fronts( fitted: &[EvaluatedChromosome>], better_than: &(impl BetterThanOperator + ?Sized) ) -> (Vec, Vec>) { let mut remaining_indices = (0..fitted.len()).collect::>(); 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, ¤t) 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>( fitted: &[EvaluatedChromosome], indices: &[usize], better_than: &(impl BetterThanOperator + ?Sized), crowding_distances: &mut [f64] ) { let mut min_maxs_init = [(Default::default(), Default::default()); OBJECTIVES]; for objective in 0..OBJECTIVES { min_maxs_init[objective].0 = fitted[indices[0]].evaluation[objective].into(); min_maxs_init[objective].1 = fitted[indices[0]].evaluation[objective].into(); } // For each objective, find minimum and maximum let min_maxs = indices .iter() .map(|&i| &fitted[i]) .fold( min_maxs_init, |mut min_maxs, current| { for objective in 0..OBJECTIVES { let mut min_max = min_maxs[objective]; let evaluation = current.evaluation[objective].into(); if evaluation > min_max.1 { min_max.1 = evaluation; } if evaluation < min_max.0 { min_max.0 = evaluation; } min_maxs[objective] = min_max; } min_maxs }); let crowding_orderings = (0..OBJECTIVES) .map(|objective| { let mut indices = indices.iter().copied().collect::>(); indices.sort_unstable_by(|&a, &b| better_than.ordering( &fitted[a].evaluation[objective], &fitted[b].evaluation[objective] )); indices }) .collect::>>(); for objective in 0..OBJECTIVES { let crowding_orderings = &crowding_orderings[objective]; let mut greaters = vec![f64::INFINITY; fitted.len()]; let mut lessers = vec![-f64::INFINITY; fitted.len()]; let mut greaters_index = 0; for i in 1..indices.len() { let current = fitted[crowding_orderings[i]].evaluation[objective].into(); let previous = fitted[crowding_orderings[i - 1]].evaluation[objective].into(); if current > previous { lessers[i] = previous; while greaters_index < i { greaters[greaters_index] = current; greaters_index += 1; } } else { lessers[i] = lessers[i - 1]; } } for (i, ¤t) in crowding_orderings.iter().enumerate() { crowding_distances[current] += (greaters[i] - lessers[i]).abs() / if NORMALIZE { (min_maxs[objective].1 - min_maxs[objective].0).abs() } else { 1.0 }; } } } pub fn get_crowding_distances>( fitted: &[EvaluatedChromosome], front_indices: &[Vec], better_than: &(impl BetterThanOperator + ?Sized) ) -> Vec { let mut crowding_distances = vec![0.0; fitted.len()]; for indices in front_indices { get_front_crowding_distances::( fitted, &indices, better_than, &mut crowding_distances ); } crowding_distances } impl<'a, const OBJECTIVES: usize, TChromosome, TOut: Copy + Into, TErr: Error> NSGAFitness<'a, OBJECTIVES, TChromosome, TOut, TErr> { pub fn new( objectives: [Box + 'a>; OBJECTIVES], better_than: &'a impl BetterThanOperator ) -> Self { Self { mo_fitness: MOFitness::<'a, OBJECTIVES, _, _, _>::new(objectives), better_than } } } impl<'a, const OBJECTIVES: usize, TChromosome: Clone, TOut: PartialEq + Default + Copy + PartialOrd + Into, TErr: Error + 'static> FitnessFunction for NSGAFitness<'a, OBJECTIVES, TChromosome, TOut, TErr> { type In = TChromosome; type Out = NSGAEvaluation; type Err = TErr; fn fit(&self, _: &Self::In) -> Result { 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) -> Result>, Self::Err> { let fitted = self.mo_fitness.fit_population(inp)?; let (fronts, front_indices) = get_nondominated_fronts(&fitted, self.better_than); let cd = get_crowding_distances ::(&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()) } } impl<'a, const OBJECTIVES: usize, const CONSTRAINTS: usize, TChromosome, TOut: Copy + Into, TErr: Error> ConstrainedNSGAFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr> { pub fn new( objectives: [Box + 'a>; OBJECTIVES], constraints: [Box>; CONSTRAINTS], better_than: &'a impl BetterThanOperator ) -> 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, TErr: Error + 'static> FitnessFunction for ConstrainedNSGAFitness<'a, OBJECTIVES, CONSTRAINTS, TChromosome, TOut, TErr> { type In = TChromosome; type Out = NSGAEvaluation; type Err = TErr; fn fit(&self, _: &Self::In) -> Result { 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) -> Result>, 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::>(); let cd = get_crowding_distances ::(&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, TErr: Error + 'static, const DParents: usize, TPairing: Pairing>, TCrossover: Crossover>, TPerturbation: PerturbationOperator> ( initial_population: Population, parents_count: usize, // TODO: possibility for objectives with different outputs? objectives: [Box + '_>; OBJECTIVES], pairing: &mut TPairing, crossover: &mut TCrossover, perturbation: &mut TPerturbation, // TODO: possibility for better_than different for different fitness functions better_than: &impl BetterThanOperator, // TODO: termination condition iterations: usize, rng: &mut dyn RngCore, mut evolutionary_strategy: impl FnMut( usize, &EvolutionStats>, &EvaluatedPopulation> // TODO ), better_than_stats: impl Fn(&TChromosome, &NSGAEvaluation, &Option>>) -> bool, ) -> Result, Box> { let result = evolution_algorithm_best_candidate( initial_population, parents_count, &mut NSGAFitness::new(objectives, 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 })) } pub fn constrained_nsga_2< const OBJECTIVES: usize, const CONSTRAINTS: usize, TChromosome: Clone, TResult: Clone + Debug + PartialEq + Default + Copy + PartialOrd + Into, TErr: Error + 'static, const DParents: usize, TPairing: Pairing>, TCrossover: Crossover>, TPerturbation: PerturbationOperator> ( initial_population: Population, parents_count: usize, // TODO: possibility for objectives with different outputs? objectives: [Box + '_>; OBJECTIVES], constraints: [Box>; CONSTRAINTS], pairing: &mut TPairing, crossover: &mut TCrossover, perturbation: &mut TPerturbation, // TODO: possibility for better_than different for different fitness functions better_than: &impl BetterThanOperator, // TODO: termination condition iterations: usize, rng: &mut dyn RngCore, mut evolutionary_strategy: impl FnMut( usize, &EvolutionStats>, &EvaluatedPopulation> // TODO ), better_than_stats: impl Fn(&TChromosome, &NSGAEvaluation, &Option>>) -> bool, ) -> Result, Box> { 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; use approx::assert_abs_diff_eq; use crate::{comparison::MinimizingOperator, multi_objective_evolution::{get_crowding_distances, get_nondominated_fronts}, population::EvaluatedChromosome, test_infra::load_test_file}; #[derive(Debug)] pub struct EvaluationResult { front: usize, crowding_distance: f64 } impl FromStr for EvaluationResult { type Err = ::Err; fn from_str(s: &str) -> Result { let mut splitted = s.split(' '); let front: f64 = splitted.next().unwrap().parse()?; let crowding_distance: f64 = splitted.next().unwrap().parse()?; Ok(EvaluationResult { front: front as usize, crowding_distance }) } } pub fn perform_test(file: &str) { let data = load_test_file::(file); let evaluations = data .iter() .map(|vec| [vec.inp[0], vec.inp[1]]) .collect::>(); println!("{:?}", evaluations); let chromosomes = evaluations .into_iter() .enumerate() .map(|(i, evaluation)| EvaluatedChromosome { chromosome: i, evaluation }) .collect::>(); let (fronts, front_indices) = get_nondominated_fronts(&chromosomes, &MinimizingOperator::new()); let crowding_distances = get_crowding_distances ::(&chromosomes, &front_indices, &MinimizingOperator::new()); assert_eq!(fronts.len(), chromosomes.len()); assert_eq!(crowding_distances.len(), chromosomes.len()); println!("{:?}", fronts); println!("{:?}", crowding_distances); for (i, test) in data.iter().enumerate() { println!("Checking index {}", i); let actual_front = fronts[i]; let actual_crowding_distance = crowding_distances[i]; assert_eq!(actual_front + 1, test.out.front); if actual_crowding_distance.is_finite() { assert_abs_diff_eq!(actual_crowding_distance, test.out.crowding_distance, epsilon = 0.0001); } else { assert_eq!(actual_crowding_distance, test.out.crowding_distance); } } } #[test] pub fn test_01() { perform_test("tests/multi_objective_1.txt"); } #[test] pub fn test_02() { perform_test("tests/multi_objective_2.txt"); } #[test] pub fn test_03() { perform_test("tests/multi_objective_3.txt"); } #[test] pub fn test_04() { perform_test("tests/multi_objective_4.txt"); } #[test] pub fn test_5() { perform_test("tests/multi_objective_5.txt"); } #[test] pub fn test_6() { perform_test("tests/multi_objective_6.txt"); } #[test] pub fn test_7() { perform_test("tests/multi_objective_7.txt"); } #[test] pub fn test_8() { perform_test("tests/multi_objective_8.txt"); } #[test] pub fn test_9() { perform_test("tests/multi_objective_9.txt"); } }