From 5119e5e33b327bb19753c11c2c609ebc0f54f3da Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 8 Nov 2025 18:37:15 +0100 Subject: [PATCH] feat(lib): constraints along with evolutionary strategies --- codes/eoa_lib/src/constraints.rs | 217 +++++++++++++++++++++++++++++++ codes/eoa_lib/src/lib.rs | 1 + 2 files changed, 218 insertions(+) create mode 100644 codes/eoa_lib/src/constraints.rs diff --git a/codes/eoa_lib/src/constraints.rs b/codes/eoa_lib/src/constraints.rs new file mode 100644 index 0000000000000000000000000000000000000000..1f879061c134940aca25ce181fb1025f1a4bd17e --- /dev/null +++ b/codes/eoa_lib/src/constraints.rs @@ -0,0 +1,217 @@ +use std::{collections::VecDeque, convert::Infallible, error::Error}; + +use thiserror::Error; + +use crate::{comparison::BetterThanOperator, crossover::Crossover, evolution::EvolutionStats, fitness::FitnessFunction, pairing::Pairing, perturbation::PerturbationOperator, population::EvaluatedPopulation, replacement::Replacement, selection::Selection}; + +pub trait ConstraintFunction { + type Chromosome; + type Out; + type Err: Error + 'static; + + fn evaluate(&self, chromosome: &Self::Chromosome) -> Result; + fn is_valid(&self, chromosome: &Self::Chromosome) -> Result; +} + +pub struct LowerThanConstraintFunction { + fun: Box TOut> +} + +impl LowerThanConstraintFunction { + pub fn new(fun: Box TOut>) -> Self { + Self { + fun + } + } +} + +impl ConstraintFunction for LowerThanConstraintFunction { + type Chromosome = TChromosome; + type Out = TOut; + type Err = Infallible; + + fn evaluate(&self, chromosome: &Self::Chromosome) -> Result { + Ok((self.fun)(chromosome)) + } + + fn is_valid(&self, chromosome: &Self::Chromosome) -> Result { + Ok(self.evaluate(chromosome)? <= Default::default()) + } +} + +pub struct ConstrainedFitnessFunction<'a, + TIn, + TOut, + TFitness: FitnessFunction, + TConstraint: ConstraintFunction> { + fitness: &'a TFitness, + constraints: Vec<&'a TConstraint>, + constraints_weight: TOut +} + +#[derive(Error, Debug)] +pub enum ConstrainedFitnessErr { + #[error("An error that came from fitness function")] + FitnessErr(T), + #[error("An error that came from constraint function")] + ConstraintErr(U) +} + +impl <'a, + TOut: std::ops::Mul + std::ops::AddAssign + Copy, + TIn, + TFitness: FitnessFunction, + TConstraint: ConstraintFunction> + FitnessFunction for ConstrainedFitnessFunction<'a, TIn, TOut, TFitness, TConstraint> { + type In = TFitness::In; + type Out = TOut; + type Err = ConstrainedFitnessErr; + + fn fit(self: &Self, inp: &Self::In) -> Result { + let mut fit = match self.fitness.fit(inp) { + Ok(fit) => fit, + Err(err) => + return Err(ConstrainedFitnessErr::FitnessErr(err)) + }; + + for &constraint in &self.constraints { + fit += self.constraints_weight * match constraint.evaluate(inp) { + Ok(constraint) => constraint, + Err(err) => + return Err(ConstrainedFitnessErr::ConstraintErr(err)) + }; + } + + Ok(fit) + } +} + +// TODO: currently these functions do recalculate the constraints for each chromosome. +// This is suboptimal. It could be solved by changing the result of fitness function to +// a tuple, where the second element of the tuple would be evaluations of the constraints. +// For this case, it would be the best if the number of constraints has been determined +// by a generic. Then, no dynamic allocation is necessary for each element. + +pub fn evolve_constraint_penalty_weight_k + , + TFitness: FitnessFunction, + TConstraint: ConstraintFunction, + TPairing: Pairing, + TCrossover: Crossover, + TReplacement: Replacement, + TPerturbation: PerturbationOperator>( + k: usize, + n: usize, + beta_1: f64, + beta_2: f64, + better_than: &impl BetterThanOperator + ) -> impl FnMut( + usize, + &EvolutionStats, + &EvaluatedPopulation, + + &mut ConstrainedFitnessFunction, + &mut TSelection, + &mut TPairing, + &mut TCrossover, + &mut TPerturbation, + &mut TReplacement + ) { + let mut k_iters_feasible = VecDeque::with_capacity(k); + move | + iteration, + _, + population, + fitness, + _, + _, + _, + _, + _| { + let best_candidate = population.best_candidate(better_than); + let feasible = fitness.constraints + .iter() + .any(|c| c.is_valid(&best_candidate.chromosome) + .expect("Can verify candidates")); + + // Change weight this iteration? + if iteration % n == 0 { + let all_feasible = k_iters_feasible.iter().all(|&f| f); + + fitness.constraints_weight *= if all_feasible { + 1.0 / beta_1 + } else { + beta_2 + } + } + + k_iters_feasible.push_back(feasible); + + if k_iters_feasible.len() > k { + k_iters_feasible.pop_front(); + } + } +} + +pub fn evolve_constraint_penalty_weight_tau_target + , + TFitness: FitnessFunction, + TConstraint: ConstraintFunction, + TPairing: Pairing, + TCrossover: Crossover, + TReplacement: Replacement, + TPerturbation: PerturbationOperator>( + n: usize, + c: f64, + tau_target: f64, + better_than: &impl BetterThanOperator + ) -> impl FnMut( + usize, + &EvolutionStats, + &EvaluatedPopulation, + + &mut ConstrainedFitnessFunction, + &mut TSelection, + &mut TPairing, + &mut TCrossover, + &mut TPerturbation, + &mut TReplacement + ) { + move | + iteration, + _, + population, + fitness, + _, + _, + _, + _, + _| { + if iteration % n != 0 { + return; + } + + let best_candidate = population.best_candidate(better_than); + let count_feasible = population.population + .iter() + .filter(|individual| { + fitness.constraints + .iter() + .all(|f| f + .is_valid(&individual.chromosome) + .expect("Can verify candidates")) + }) + .count(); + let tau = count_feasible as f64 / population.population.len() as f64; + + if tau > tau_target { + fitness.constraints_weight *= 1.0 / c; + } else { + fitness.constraints_weight *= c; + } + } +} diff --git a/codes/eoa_lib/src/lib.rs b/codes/eoa_lib/src/lib.rs index 34fd57d1b472d47dd0b14a98f84431fb08bcb688..c02b947853f1192047a439b0236a1677a17d6ea3 100644 --- a/codes/eoa_lib/src/lib.rs +++ b/codes/eoa_lib/src/lib.rs @@ -14,6 +14,7 @@ pub mod local_search; pub mod random_search; pub mod binary_string; pub mod evolutionary_strategy; +pub mod constraints; #[cfg(test)] mod test_infra;