From 8b73ac0a6cd9d2d1ea618f9325fb59a74c5628ac Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 30 Nov 2025 18:33:27 +0100 Subject: [PATCH] feat: add bounded crossover similar to bounded perturbation --- codes/eoa_lib/src/crossover.rs | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/codes/eoa_lib/src/crossover.rs b/codes/eoa_lib/src/crossover.rs index 7fc08373939a5d1249de75a1f03f5ede976355ef..ebd1c109d0f9d1740a396be849e6348530e37343 100644 --- a/codes/eoa_lib/src/crossover.rs +++ b/codes/eoa_lib/src/crossover.rs @@ -455,3 +455,115 @@ where Population::from_vec(offsprings) } } + +pub enum BoundedCrossoverStrategy { + /// Trims offspring values to get values within bounds + Trim, + /// Retries calling the underlying crossover until offspring within bounds is returned. + /// If argument is given, this is the maximum number of retries to do and then + /// fall back to trimming. Zero means retry indefinitely. + Retry(usize) +} + +pub struct BoundedCrossover>> +where + DefaultAllocator: Allocator +{ + min_max: OVector<(f64, f64), D>, + strategy: BoundedCrossoverStrategy, + crossover: T, + _phantom: PhantomData [(); DParents]> +} + +impl>> BoundedCrossover +where + DefaultAllocator: Allocator +{ + pub fn new( + crossover: T, + min: OVector, + max: OVector, + strategy: BoundedCrossoverStrategy + ) -> Self { + let min_max = min.zip_map(&max, |min, max| (min, max)); + Self { + min_max, + strategy, + crossover, + _phantom: PhantomData + } + } + + fn within_bounds(&self, chromosome: &OVector) -> bool { + chromosome.iter() + .zip(self.min_max.iter()) + .all(|(&c, &(min, max))| c <= max && c >= min) + } + + fn bound(&self, mut chromosome: OVector) -> OVector { + chromosome + .zip_apply(&self.min_max, |c, (min, max)| *c = c.clamp(min, max)); + chromosome + } + + fn bound_population(&self, mut population: Population>) -> Population> { + for chromosome in population.iter_mut() { + *chromosome = self.bound(chromosome.clone()); + } + population + } + + fn all_within_bounds(&self, population: &Population>) -> bool { + population.iter().all(|chromosome| self.within_bounds(chromosome)) + } + + fn retry_crossover( + &self, + population: &EvaluatedPopulation, TOut>, + pairs: Vec>, + retries: Option, + rng: &mut dyn RngCore + ) -> Population> + where + T: Crossover, Out = TOut> + { + let offspring = self.crossover.crossover(population, pairs.clone().into_iter(), rng); + + if self.all_within_bounds(&offspring) { + return offspring; + } + + match retries { + Some(0) | None => self.bound_population(offspring), + Some(retries) => { + self.retry_crossover(population, pairs, Some(retries - 1), rng) + } + } + } +} + +impl Crossover for BoundedCrossover +where + T: Crossover, Out = TOut>, + DefaultAllocator: Allocator +{ + type Chromosome = OVector; + type Out = TOut; + + fn crossover( + &self, + population: &EvaluatedPopulation, + pairs: impl Iterator>, + rng: &mut dyn RngCore + ) -> Population { + match self.strategy { + BoundedCrossoverStrategy::Trim => { + self.bound_population(self.crossover.crossover(population, pairs, rng)) + }, + BoundedCrossoverStrategy::Retry(retries) => { + let pairs_vec: Vec<_> = pairs.collect(); + self.retry_crossover(population, pairs_vec, Some(retries), rng) + } + } + } +}