From 458589fdc2ace8fd18ebc5f808ee3a697d4f24f8 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Fri, 5 Dec 2025 20:08:36 +0100 Subject: [PATCH] feat: implement constrained nsga --- .../eoa_lib/src/multi_objective_evolution.rs | 290 +++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/codes/eoa_lib/src/multi_objective_evolution.rs b/codes/eoa_lib/src/multi_objective_evolution.rs index 7373c02e008a099c758f03f4f486d9a49fb2037d..ec8d9a061b6486cc63dce671dabb414dd0a85043 100644 --- a/codes/eoa_lib/src/multi_objective_evolution.rs +++ b/codes/eoa_lib/src/multi_objective_evolution.rs @@ -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 + '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) @@ -24,6 +36,31 @@ pub fn a_dominates_b( 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], @@ -32,6 +69,13 @@ pub fn a_dominated_by_b( 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, @@ -65,6 +109,69 @@ impl<'a, } } +#[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], @@ -90,7 +197,12 @@ pub struct NSGAFitness<'a, const OBJECTIVES: usize, TChromosome, TOut, TErr: Err better_than: &'a dyn BetterThanOperator } -pub fn get_nondominated_fronts( +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>) { @@ -144,6 +256,60 @@ pub fn get_nondominated_fronts( (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, + 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, @@ -354,6 +587,61 @@ 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], + 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;