From dfddcfde9cade6fd94ace5bca2f846f48c230e3c Mon Sep 17 00:00:00 2001 From: Rutherther Date: Thu, 2 Oct 2025 21:29:39 +0200 Subject: [PATCH] chore: split types and functions to separate module files --- env/src/binary_string.rs | 285 ++++++++++++ env/src/comparison/mod.rs | 18 + env/src/fitness/labs.rs | 54 +++ env/src/fitness/mod.rs | 43 ++ env/src/fitness/one_max.rs | 36 ++ env/src/fitness/rosenbrock.rs | 36 ++ env/src/fitness/sphere.rs | 42 ++ env/src/local_search/mod.rs | 147 ++++++ env/src/main.rs | 852 +--------------------------------- env/src/perturbation/mod.rs | 50 ++ env/src/terminating/mod.rs | 106 +++++ env/src/test_infra.rs | 78 ++++ 12 files changed, 903 insertions(+), 844 deletions(-) create mode 100644 env/src/binary_string.rs create mode 100644 env/src/comparison/mod.rs create mode 100644 env/src/fitness/labs.rs create mode 100644 env/src/fitness/mod.rs create mode 100644 env/src/fitness/one_max.rs create mode 100644 env/src/fitness/rosenbrock.rs create mode 100644 env/src/fitness/sphere.rs create mode 100644 env/src/local_search/mod.rs create mode 100644 env/src/perturbation/mod.rs create mode 100644 env/src/terminating/mod.rs create mode 100644 env/src/test_infra.rs diff --git a/env/src/binary_string.rs b/env/src/binary_string.rs new file mode 100644 index 0000000000000000000000000000000000000000..9aa670da780c2d0e39c84ed4e67e52d1da310112 --- /dev/null +++ b/env/src/binary_string.rs @@ -0,0 +1,285 @@ +use std::str::FromStr; +use crate::test_infra::{load_test_file, DataArrOfReals}; + +use rand::Rng; + +#[derive(Debug, Clone, PartialEq)] +pub struct Bounds { + min: f64, + max: f64, +} + +impl Bounds { + pub fn new(min: f64, max: f64) -> Self { + Bounds { + min, + max + } + } + + pub fn min(&self) -> f64 { + self.min + } + + pub fn max(&self) -> f64 { + self.max + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct BinaryString { + pub vec: Vec +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BinaryStringConversionError { + DimensionMismatch, + NoBounds +} + +impl BinaryString { + pub fn new(vec: Vec) -> BinaryString { + BinaryString { + vec + } + } + + pub fn vec(&self) -> &Vec { + &self.vec + } + + pub fn perturb(self: &Self, rng: &mut TRng, p: f64) -> Self + where TRng : Rng + { + BinaryString::new( + self.into_iter() + .map(|c| if rng.random::() <= p { 1 - *c } else { *c }) + .collect::>() + ) + } + + fn to_real_internal<'a, T: DoubleEndedIterator>(vec: T, len: usize, min: f64, max: f64) -> f64 + { + let diff = max - min; + let len = len as i32; + let max_represent_num = 2f64.powi(len) - 1.0; + let represented_num = vec + .rev() + .enumerate() + .map(|(bit, c)| diff * (*c as f64) * 2f64.powi(bit as i32)) + .sum::(); + + min + (represented_num / max_represent_num) + } + + pub fn to_real_single(self: &Self, min: f64, max: f64) -> f64 { + BinaryString::to_real_internal(self.vec.iter(), self.vec.len(), min, max) + } + + pub fn to_real(self: &Self, bounds: &Vec) -> Result, BinaryStringConversionError> { + if bounds.len() == 0 { + return Err(BinaryStringConversionError::NoBounds); + } + + let chunk_size = self.vec.len() / bounds.len(); + if self.vec.len() % bounds.len() != 0 { + return Err(BinaryStringConversionError::DimensionMismatch); + } + + Ok(self.vec.chunks(chunk_size) + .zip(bounds) + .map(|(chunk, bound)| BinaryString::to_real_internal(chunk.iter(), chunk_size, bound.min(), bound.max())) + .collect::>()) + } +} + +impl FromStr for BinaryString { + type Err = String; + + fn from_str(s: &str) -> Result { + let binary_vec: Vec = s + .chars() + // skip spaces + .filter(|c| *c != ' ') + // Map ones and zeros + .map(|c| match c { + '0' => Ok(0), + '1' => Ok(1), + _ => Err(format!("Invalid binary character: {}", c)), + }) + .collect::, Self::Err>>()?; + + Ok(BinaryString::new(binary_vec)) + } +} + +impl<'a> IntoIterator for &'a BinaryString { + type Item = &'a i8; + type IntoIter = std::slice::Iter<'a, i8>; + + fn into_iter(self) -> Self::IntoIter { + self.vec.iter() + } +} + +#[test] +fn test_binary_string_to_real_single() { + assert_eq!( + BinaryString::new(vec![1]) + .to_real_single(0.0, 32.0), + 32.0 + ); + assert_eq!( + BinaryString::new(vec![1, 1]) + .to_real_single(0.0, 32.0), + 32.0 + ); + assert_eq!( + BinaryString::new(vec![0, 1]) + .to_real_single(0.0, 32.0), + 32.0 / 3.0 + ); + assert_eq!( + BinaryString::new(vec![0, 0]) + .to_real_single(-16.0, 16.0), + -16.0 + ); + assert_eq!( + BinaryString::new(vec![0, 0, 0, 0, 1]) + .to_real_single(0.0, 31.0), + 1.0 + ); + assert_eq!( + BinaryString::new(vec![1, 1, 1, 1, 1]) + .to_real_single(0.0, 31.0), + 31.0 + ); + assert_eq!( + BinaryString::new(vec![0, 0, 0, 1, 0]) + .to_real_single(0.0, 31.0), + 2.0 + ); + + assert_eq!( + BinaryString::new(vec![1; 512]) + .to_real_single(0.0, 31.0), + 31.0 + ); +} + +fn test_binary_string_to_real(file_name: &str, bounds: Vec) { + let data = load_test_file::(file_name); + + for test in data { + let res = BinaryString::new(test.inp) + .to_real(&bounds); + if !test.out.valid { + assert_eq!( + res, + Err(BinaryStringConversionError::DimensionMismatch) + ); + } + else { + assert_eq!( + res.unwrap(), + test.out.vec + ); + } + } +} + +#[test] +fn test_binary_string_to_real_1D_1() { + test_binary_string_to_real( + "tests/Bin2Real_1D_1.txt", + vec![Bounds::new(0.0, 1.0)] + ); +} + +#[test] +fn test_binary_string_to_real_1D_2() { + test_binary_string_to_real( + "tests/Bin2Real_1D_2.txt", + vec![Bounds::new(0.0, 4095.0)] + ); +} + +#[test] +fn test_binary_string_to_real_1D_3() { + test_binary_string_to_real( + "tests/Bin2Real_1D_3.txt", + vec![Bounds::new(-5.0, 5.0)] + ); +} + +#[test] +fn test_binary_string_to_real_2D_1() { + test_binary_string_to_real( + "tests/Bin2Real_2D_1.txt", + vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] + ); +} + +#[test] +fn test_binary_string_to_real_2D_2() { + test_binary_string_to_real( + "tests/Bin2Real_2D_2.txt", + vec![Bounds::new(0.0, 63.0), Bounds::new(-32.0, 31.0)] + ); +} + +#[test] +fn test_binary_string_to_real_2D_3() { + test_binary_string_to_real( + "tests/Bin2Real_2D_3.txt", + vec![Bounds::new(-5.0, 5.0), Bounds::new(0.0, 10.0)] + ); +} + +#[test] +fn test_binary_string_to_real_3D_1() { + test_binary_string_to_real( + "tests/Bin2Real_3D_1.txt", + vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] + ); +} + +#[test] +fn test_binary_string_to_real_3D_2() { + test_binary_string_to_real( + "tests/Bin2Real_3D_2.txt", + vec![Bounds::new(0.0, 15.0), Bounds::new(-8.0, 7.0), Bounds::new(-8.0, 8.0)] + ); +} + +#[test] +fn test_binary_string_to_real_4D_1() { + test_binary_string_to_real( + "tests/Bin2Real_4D_1.txt", + vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] + ); +} + +#[test] +fn test_binary_string_to_real_4D_2() { + test_binary_string_to_real( + "tests/Bin2Real_4D_2.txt", + vec![Bounds::new(0.0, 7.0), Bounds::new(-4.0, 3.0), Bounds::new(-4.0, 4.0), Bounds::new(-8.0, 0.0)] + ); +} + +#[test] +fn test_binary_string_to_real_6D_1() { + test_binary_string_to_real( + "tests/Bin2Real_6D_1.txt", + vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] + ); +} + +#[test] +fn test_binary_string_to_real_6D_2() { + test_binary_string_to_real( + "tests/Bin2Real_6D_2.txt", + (0..=5).map(|x| Bounds::new(x as f64, (2 * (x + 1)) as f64)).collect::>() + ); +} diff --git a/env/src/comparison/mod.rs b/env/src/comparison/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..82601af51e3a0bd19b07f8ce39759132331a92ac --- /dev/null +++ b/env/src/comparison/mod.rs @@ -0,0 +1,18 @@ +pub trait BetterThanOperator { + fn better_than(self: &Self, a: &T, b: &T) -> bool; +} + +pub struct MinimizingOperator; +impl MinimizingOperator { + pub fn new() -> Self { + Self + } +} + +impl BetterThanOperator for MinimizingOperator + where T: PartialOrd +{ + fn better_than(self: &Self, a: &T, b: &T) -> bool { + a < b + } +} diff --git a/env/src/fitness/labs.rs b/env/src/fitness/labs.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb1a494bbff0e0595f8d89bdb7122222b4a74ade --- /dev/null +++ b/env/src/fitness/labs.rs @@ -0,0 +1,54 @@ +use std::convert::Infallible; +use crate::{binary_string::BinaryString, test_infra::load_test_file}; +use super::FitnessFunction; + +pub struct LABS; + +impl LABS { + fn new() -> Self { + LABS + } + + fn Ck(k: usize, S: &Vec) -> i32 { + let D = S.len(); + S.iter() + .take(D - k) + .zip(S.iter().skip(k)) + .map(|(x, y)| x * y) + .sum() + } +} + +impl FitnessFunction for LABS { + type In = BinaryString; + type Out = i32; + type Err = Infallible; + + fn fit(self: &Self, chromosome: &BinaryString) -> Result { + let S: Vec = chromosome + .into_iter() + .map(|c| (*c as i32) * 2 - 1) + .collect(); + let D = S.len(); + + Ok((1..=D-1) + .map(|k| LABS::Ck(k, &S).pow(2)) + .sum()) + } +} + +#[test] +fn test_LABS() { + let data = load_test_file::("tests/labs.txt"); + + for test in data { + println!("Test vector {}", test.inp.iter() + .map(|x| x.to_string()) + .collect::>() + .join(", ")); + assert_eq!( + LABS::new().fit(&BinaryString::new(test.inp)).unwrap(), + test.out + ) + } +} diff --git a/env/src/fitness/mod.rs b/env/src/fitness/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..271aa73751ec3bcb6f50910c73091963e3d3a07c --- /dev/null +++ b/env/src/fitness/mod.rs @@ -0,0 +1,43 @@ +use std::convert::Infallible; + +use crate::binary_string::{BinaryString, BinaryStringConversionError, Bounds}; + +pub mod labs; +pub mod one_max; +pub mod rosenbrock; +pub mod sphere; + +pub trait FitnessFunction { + type In; + type Out; + type Err; + + fn fit(self: &Self, inp: &Self::In) -> Result; +} + +pub struct BinaryFitnessWrapper { + bounds: Vec, + fitting_function: TFitness, +} + +impl BinaryFitnessWrapper { + pub fn new(fitting_function: TFitness, bounds: Vec) -> Self { + BinaryFitnessWrapper { + fitting_function, + bounds, + } + } +} + +impl FitnessFunction for BinaryFitnessWrapper +where + TFitness: FitnessFunction, Out = f64, Err = Infallible> +{ + type In = BinaryString; + type Out = f64; + type Err = BinaryStringConversionError; + + fn fit(self: &Self, inp: &BinaryString) -> Result { + Ok(self.fitting_function.fit(&inp.to_real(&self.bounds)?).unwrap()) + } +} diff --git a/env/src/fitness/one_max.rs b/env/src/fitness/one_max.rs new file mode 100644 index 0000000000000000000000000000000000000000..b80e3b7d5fa44ad04cf1765f6446df0a40f86073 --- /dev/null +++ b/env/src/fitness/one_max.rs @@ -0,0 +1,36 @@ +use std::convert::Infallible; + +use crate::{binary_string::BinaryString, test_infra::load_test_file}; + +use super::FitnessFunction; + +pub struct OneMax; +impl OneMax { + pub fn new() -> Self { + OneMax + } +} + +impl FitnessFunction for OneMax { + type In = BinaryString; + type Out = i32; + type Err = Infallible; + + fn fit(self: &Self, chromosome: &BinaryString) -> Result { + Ok(chromosome.into_iter() + .map(|x| *x as i32) + .sum()) + } +} + +#[test] +fn test_one_max() { + let data = load_test_file::("tests/onemax.txt"); + + for test in data { + assert_eq!( + OneMax::new().fit(&BinaryString::new(test.inp)).unwrap(), + test.out + ); + } +} diff --git a/env/src/fitness/rosenbrock.rs b/env/src/fitness/rosenbrock.rs new file mode 100644 index 0000000000000000000000000000000000000000..e7f9cfda89052305ea2664cff6a6ac620aeded12 --- /dev/null +++ b/env/src/fitness/rosenbrock.rs @@ -0,0 +1,36 @@ +use std::convert::Infallible; +use crate::test_infra::load_test_file; + +use super::FitnessFunction; + +pub struct Rosenbrock; + +impl Rosenbrock { + pub fn new() -> Self { + Rosenbrock + } +} + +impl FitnessFunction for Rosenbrock { + type In = Vec; + type Out = f64; + type Err = Infallible; + + fn fit(self: &Self, inp: &Vec) -> Result { + Ok(inp.windows(2) + .map(|xs| 100.0 * (xs[1] - xs[0].powi(2)).powi(2) + (1.0 - xs[0]).powi(2)) + .sum()) + } +} + +#[test] +fn test_rosenbrock() { + let data = load_test_file::("tests/rosenbrock.txt"); + + for test in data { + assert_eq!( + Rosenbrock::new().fit(&test.inp).unwrap(), + test.out + ) + } +} diff --git a/env/src/fitness/sphere.rs b/env/src/fitness/sphere.rs new file mode 100644 index 0000000000000000000000000000000000000000..ece1f4284848044e1a3bb8dba1645c2a38c78078 --- /dev/null +++ b/env/src/fitness/sphere.rs @@ -0,0 +1,42 @@ +use std::convert::Infallible; +use crate::test_infra::load_test_file; + +use super::FitnessFunction; + +pub struct Sphere { + offset: Vec +} + +impl Sphere { + pub fn new(offset: Vec) -> Self { + Sphere { + offset + } + } +} + +impl FitnessFunction for Sphere { + type In = Vec; + type Out = f64; + type Err = Infallible; + + fn fit(self: &Self, chromosome: &Vec) -> Result { + Ok(chromosome + .iter() + .zip(&self.offset) + .map(|(x, o)| (x - o).powi(2)) + .sum()) + } +} + +#[test] +fn test_sphere() { + let data = load_test_file::("tests/sphere.txt"); + + for test in data { + assert_eq!( + Sphere::new(vec![1.0; 10]).fit(&test.inp).unwrap(), + test.out + ) + } +} diff --git a/env/src/local_search/mod.rs b/env/src/local_search/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..56a00d12c903deeb4bfdc2f1643a72780ca26d51 --- /dev/null +++ b/env/src/local_search/mod.rs @@ -0,0 +1,147 @@ +use crate::binary_string::{BinaryString, Bounds}; +use crate::fitness::one_max::OneMax; +use crate::fitness::sphere::Sphere; +use crate::terminating::{AndTerminatingConditions, EqualTerminatingCondition, NoBetterForCyclesTerminatingCondition, TerminatingCondition}; +use crate::perturbation::{BinaryStringBitPerturbation, PerturbationOperator}; +use crate::comparison::{BetterThanOperator, MinimizingOperator}; +use crate::fitness::{FitnessFunction, BinaryFitnessWrapper}; + +// Functions +#[derive(Debug, Clone, PartialEq)] +pub struct LocalSearchCandidate + where T: Clone +{ + pub fit: T, + pub pos: BinaryString, + pub cycle: usize +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LocalSearchResult + where T: Clone +{ + pub best_candidate: LocalSearchCandidate, + + // How many cycles there were + pub cycles: usize, + + // statistics + pub best_candidates: Vec> +} + +fn local_search_first_improving< + T, TErr, TFit, TTerminatingCondition, TPerturbationOperator, TBetterThanOperator>( + fit: &TFit, + terminating_condition: &mut TTerminatingCondition, + perturbation_operator: &mut TPerturbationOperator, + better_than_operator: &TBetterThanOperator, + initial: &BinaryString +) -> Result, TErr> +where + T: Clone, + TFit: FitnessFunction, + TTerminatingCondition: TerminatingCondition, + TPerturbationOperator: PerturbationOperator, + TBetterThanOperator: BetterThanOperator, +{ + let mut best_candidate = LocalSearchCandidate { + pos: initial.clone(), + fit: fit.fit(&initial)?, + cycle: 0 + }; + + let mut stats: Vec> = vec![]; + let mut cycle: usize = 0; + + while !terminating_condition.should_terminate(&best_candidate, &stats, cycle) { + let perturbed = perturbation_operator.perturb(&best_candidate.pos); + let perturbed_fit = fit.fit(&perturbed)?; + + // Minimize + if better_than_operator.better_than(&perturbed_fit, &best_candidate.fit) { + best_candidate = LocalSearchCandidate { + pos: perturbed.clone(), + fit: perturbed_fit, + cycle + }; + + stats.push(best_candidate.clone()); + } + + cycle += 1; + } + + Ok(LocalSearchResult { + best_candidate, + best_candidates: stats, + cycles: cycle + }) +} + +#[test] +fn test_local_search_sphere() { + let optimum = BinaryString::new(vec![0, 0, 1, 0, 0, + 0, 0, 1, 0, 0]); + let bounds = vec![Bounds::new(0.0, 31.0), Bounds::new(0.0, 31.0)]; + let optimum_real = optimum.to_real(&bounds).unwrap(); + let sphere = Sphere::new(optimum_real); + let sphere_wrapped = BinaryFitnessWrapper::new(sphere, bounds); + + let result = local_search_first_improving( + &sphere_wrapped, + &mut + AndTerminatingConditions::new( + vec![ + &mut EqualTerminatingCondition::new_remembered(optimum.clone()), + &mut NoBetterForCyclesTerminatingCondition::new(100) + ] + ), + &mut BinaryStringBitPerturbation::new(0.3), + &MinimizingOperator::new(), + &BinaryString::new(vec![1; 10]), + ).unwrap(); + + println!("{:?}", result); + + assert_eq!( + result.best_candidate.fit, + 0.0 + ); + + assert_eq!( + result.best_candidate.pos, + optimum + ); +} + +#[test] +fn test_local_search_one_max() { + let one_max = OneMax::new(); + let optimum = BinaryString::new(vec![0; 10]); + + let result = local_search_first_improving( + &one_max, + &mut + AndTerminatingConditions::new( + vec![ + &mut EqualTerminatingCondition::new_remembered(optimum.clone()), + &mut NoBetterForCyclesTerminatingCondition::new(100) + ] + ), + &mut BinaryStringBitPerturbation::new(0.3), + &MinimizingOperator::new(), + &BinaryString::new(vec![1; 10]), + ).unwrap(); + + println!("{:?}", result); + + assert_eq!( + result.best_candidate.fit, + 0 + ); + + assert_eq!( + result.best_candidate.pos, + optimum + ); +} diff --git a/env/src/main.rs b/env/src/main.rs index 74e8f72e10a711879304af0d1d046115e79af0e5..aa951990f06302aab66f6406ca42147d37a460f3 100644 --- a/env/src/main.rs +++ b/env/src/main.rs @@ -1,848 +1,12 @@ -use std::{convert::Infallible, fmt::{Binary, Debug}, fs::File, io::{BufRead, BufReader}, marker::PhantomData, num::ParseFloatError, str::FromStr}; -use rand::Rng; +pub mod fitness; +pub mod terminating; +pub mod perturbation; +pub mod comparison; +pub mod local_search; +pub mod binary_string; -fn add(a: u16, b: u16) -> u16 { - a + b -} - -#[derive(Debug, Clone, PartialEq)] -struct BinaryString { - vec: Vec -} - -struct Bounds { - min: f64, - max: f64, -} - -impl Bounds { - pub fn new(min: f64, max: f64) -> Self { - Bounds { - min, - max - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum BinaryStringConversionError { - DimensionMismatch, - NoBounds -} - -impl BinaryString { - pub fn new(vec: Vec) -> BinaryString { - BinaryString { - vec - } - } - - pub fn perturb(self: &Self, rng: &mut TRng, p: f64) -> Self - where TRng : Rng - { - BinaryString::new( - self.into_iter() - .map(|c| if rng.random::() <= p { 1 - *c } else { *c }) - .collect::>() - ) - } - - fn to_real_internal<'a, T: DoubleEndedIterator>(vec: T, len: usize, min: f64, max: f64) -> f64 - { - let diff = max - min; - let len = len as i32; - let max_represent_num = 2f64.powi(len) - 1.0; - let represented_num = vec - .rev() - .enumerate() - .map(|(bit, c)| diff * (*c as f64) * 2f64.powi(bit as i32)) - .sum::(); - - min + (represented_num / max_represent_num) - } - - pub fn to_real_single(self: &Self, min: f64, max: f64) -> f64 { - BinaryString::to_real_internal(self.vec.iter(), self.vec.len(), min, max) - } - - pub fn to_real(self: &Self, bounds: &Vec) -> Result, BinaryStringConversionError> { - if bounds.len() == 0 { - return Err(BinaryStringConversionError::NoBounds); - } - - let chunk_size = self.vec.len() / bounds.len(); - if self.vec.len() % bounds.len() != 0 { - return Err(BinaryStringConversionError::DimensionMismatch); - } - - Ok(self.vec.chunks(chunk_size) - .zip(bounds) - .map(|(chunk, bound)| BinaryString::to_real_internal(chunk.iter(), chunk_size, bound.min, bound.max)) - .collect::>()) - } -} - -impl FromStr for BinaryString { - type Err = String; - - fn from_str(s: &str) -> Result { - let binary_vec: Vec = s - .chars() - // skip spaces - .filter(|c| *c != ' ') - // Map ones and zeros - .map(|c| match c { - '0' => Ok(0), - '1' => Ok(1), - _ => Err(format!("Invalid binary character: {}", c)), - }) - .collect::, Self::Err>>()?; - - Ok(BinaryString::new(binary_vec)) - } -} - -impl<'a> IntoIterator for &'a BinaryString { - type Item = &'a i8; - type IntoIter = std::slice::Iter<'a, i8>; - - fn into_iter(self) -> Self::IntoIter { - self.vec.iter() - } -} - -trait FittingFunction { - type In; - type Out; - type Err; - - fn fit(self: &Self, inp: &Self::In) -> Result; -} - -// Functions -struct OneMax; -impl OneMax { - pub fn new() -> Self { - OneMax - } -} -impl FittingFunction for OneMax { - type In = BinaryString; - type Out = i32; - type Err = Infallible; - - fn fit(self: &Self, chromosome: &BinaryString) -> Result { - Ok(chromosome.into_iter() - .map(|x| *x as i32) - .sum()) - } -} -struct LABS; - -impl LABS { - fn new() -> Self { - LABS - } - - fn Ck(k: usize, S: &Vec) -> i32 { - let D = S.len(); - S.iter() - .take(D - k) - .zip(S.iter().skip(k)) - .map(|(x, y)| x * y) - .sum() - } -} - -impl FittingFunction for LABS { - type In = BinaryString; - type Out = i32; - type Err = Infallible; - - fn fit(self: &Self, chromosome: &BinaryString) -> Result { - let S: Vec = chromosome - .into_iter() - .map(|c| (*c as i32) * 2 - 1) - .collect(); - let D = S.len(); - - Ok((1..=D-1) - .map(|k| LABS::Ck(k, &S).pow(2)) - .sum()) - } -} - -struct Sphere { - offset: Vec -} - -impl Sphere { - pub fn new(offset: Vec) -> Self { - Sphere { - offset - } - } -} -impl FittingFunction for Sphere { - type In = Vec; - type Out = f64; - type Err = Infallible; - - fn fit(self: &Self, chromosome: &Vec) -> Result { - Ok(chromosome - .iter() - .zip(&self.offset) - .map(|(x, o)| (x - o).powi(2)) - .sum()) - } -} - -struct Rosenbrock; - -impl Rosenbrock { - pub fn new() -> Self { - Rosenbrock - } -} - -impl FittingFunction for Rosenbrock { - type In = Vec; - type Out = f64; - type Err = Infallible; - - fn fit(self: &Self, inp: &Vec) -> Result { - Ok(inp.windows(2) - .map(|xs| 100.0 * (xs[1] - xs[0].powi(2)).powi(2) + (1.0 - xs[0]).powi(2)) - .sum()) - } -} - -struct BinaryFittingWrapper { - bounds: Vec, - fitting_function: TFitting, -} - -impl BinaryFittingWrapper { - pub fn new(fitting_function: TFitting, bounds: Vec) -> Self { - BinaryFittingWrapper { - fitting_function, - bounds, - } - } -} - -impl FittingFunction for BinaryFittingWrapper -where - TFitting: FittingFunction, Out = f64, Err = Infallible> -{ - type In = BinaryString; - type Out = f64; - type Err = BinaryStringConversionError; - - fn fit(self: &Self, inp: &BinaryString) -> Result { - Ok(self.fitting_function.fit(&inp.to_real(&self.bounds)?).unwrap()) - } -} - -#[derive(Debug, Clone, PartialEq)] -struct LocalSearchCandidate - where T: Clone -{ - fit: T, - pos: BinaryString, - cycle: usize -} - -#[derive(Debug, Clone, PartialEq)] -struct LocalSearchResult - where T: Clone -{ - best_candidate: LocalSearchCandidate, - - // How many cycles there were - cycles: usize, - - // statistics - best_candidates: Vec> -} - -trait TerminatingCondition - where T: Clone -{ - fn should_terminate( - self: &mut Self, - candidate: &LocalSearchCandidate, - stats: &Vec>, - cycle: usize - ) -> bool; -} - -struct EqualTerminatingCondition { - target: BinaryString, - remember_match: bool, - matched: bool, -} - -impl EqualTerminatingCondition { - pub fn new(target: BinaryString) -> Self { - Self { - target, - remember_match: false, - matched: false, - } - } - - pub fn new_remembered(target: BinaryString) -> Self { - Self { - target, - remember_match: true, - matched: false, - } - } - - pub fn reset_match(self: &mut Self) { - self.matched = false; - } -} - -impl TerminatingCondition for EqualTerminatingCondition { - fn should_terminate( - self: &mut Self, - candidate: &LocalSearchCandidate, - _: &Vec>, - _: usize - ) -> bool { - let matched = candidate.pos == self.target; - - if matched && self.remember_match { - self.matched = true; - } - - matched || self.matched - } -} - -struct NoBetterForCyclesTerminatingCondition { - cycles: usize -} - -impl NoBetterForCyclesTerminatingCondition { - pub fn new(cycles: usize) -> Self { - Self { - cycles - } - } -} - -struct AndTerminatingConditions<'a, T: Clone> { - terminating_conditions: Vec<&'a mut dyn TerminatingCondition> -} - -impl<'a, T: Clone> AndTerminatingConditions<'a, T> { - pub fn new(terminating_conditions: Vec<&'a mut dyn TerminatingCondition>) -> Self { - Self { - terminating_conditions - } - } -} - -impl<'a, T: Clone> TerminatingCondition for AndTerminatingConditions<'a, T> { - fn should_terminate( - self: &mut Self, - candidate: &LocalSearchCandidate, - stats: &Vec>, - cycle: usize - ) -> bool { - return self.terminating_conditions.iter_mut() - .all( - |cond| cond.should_terminate(candidate, stats, cycle) - ) - } -} - -impl TerminatingCondition for NoBetterForCyclesTerminatingCondition { - fn should_terminate ( - self: &mut Self, - candidate: &LocalSearchCandidate, - _: &Vec>, - cycle: usize - ) -> bool { - (cycle - candidate.cycle) > self.cycles - } -} - -trait PerturbationOperator { - type Chromosome; - - fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome; -} - -struct BinaryStringBitPerturbation { - rng: TRng, - p: f64, -} - -impl BinaryStringBitPerturbation { - pub fn new(p: f64) -> Self { - Self { - rng: rand::rng(), - p - } - } -} - -impl PerturbationOperator for BinaryStringBitPerturbation { - type Chromosome = BinaryString; - - fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome { - chromosome.perturb(&mut self.rng, self.p) - } -} - -trait BetterThanOperator { - fn better_than(self: &Self, a: &T, b: &T) -> bool; -} - -struct DefaultBetterThan; -impl DefaultBetterThan { - pub fn new() -> Self { - Self - } -} - -impl BetterThanOperator for DefaultBetterThan - where T: PartialOrd -{ - fn better_than(self: &Self, a: &T, b: &T) -> bool { - a < b - } -} - -fn local_search_first_improving< - T, TErr, TFit, TTerminatingCondition, TPerturbationOperator, TBetterThanOperator>( - fit: &TFit, - terminating_condition: &mut TTerminatingCondition, - perturbation_operator: &mut TPerturbationOperator, - better_than_operator: &TBetterThanOperator, - initial: &BinaryString -) -> Result, TErr> -where - T: Clone, - TFit: FittingFunction, - TTerminatingCondition: TerminatingCondition, - TPerturbationOperator: PerturbationOperator, - TBetterThanOperator: BetterThanOperator, -{ - let mut best_candidate = LocalSearchCandidate { - pos: initial.clone(), - fit: fit.fit(&initial)?, - cycle: 0 - }; - - let mut stats: Vec> = vec![]; - let mut cycle: usize = 0; - - while !terminating_condition.should_terminate(&best_candidate, &stats, cycle) { - let perturbed = perturbation_operator.perturb(&best_candidate.pos); - let perturbed_fit = fit.fit(&perturbed)?; - - // Minimize - if better_than_operator.better_than(&perturbed_fit, &best_candidate.fit) { - best_candidate = LocalSearchCandidate { - pos: perturbed.clone(), - fit: perturbed_fit, - cycle - }; - - stats.push(best_candidate.clone()); - } - - cycle += 1; - } - - Ok(LocalSearchResult { - best_candidate, - best_candidates: stats, - cycles: cycle - }) -} - -#[test] -fn test_local_search_one_max() { - let one_max = OneMax::new(); - let optimum = BinaryString::new(vec![0; 10]); - - let result = local_search_first_improving( - &one_max, - &mut - AndTerminatingConditions::new( - vec![ - &mut EqualTerminatingCondition::new_remembered(optimum.clone()), - &mut NoBetterForCyclesTerminatingCondition::new(100) - ] - ), - &mut BinaryStringBitPerturbation::new(0.3), - &DefaultBetterThan::new(), - &BinaryString::new(vec![1; 10]), - ).unwrap(); - - println!("{:?}", result); - - assert_eq!( - result.best_candidate.fit, - 0 - ); - - assert_eq!( - result.best_candidate.pos, - optimum - ); -} - -#[test] -fn test_local_search_sphere() { - let optimum = BinaryString::new(vec![0, 0, 1, 0, 0, - 0, 0, 1, 0, 0]); - let bounds = vec![Bounds::new(0.0, 31.0), Bounds::new(0.0, 31.0)]; - let optimum_real = optimum.to_real(&bounds).unwrap(); - let sphere = Sphere::new(optimum_real); - let sphere_wrapped = BinaryFittingWrapper::new(sphere, bounds); - - let result = local_search_first_improving( - &sphere_wrapped, - &mut - AndTerminatingConditions::new( - vec![ - &mut EqualTerminatingCondition::new_remembered(optimum.clone()), - &mut NoBetterForCyclesTerminatingCondition::new(100) - ] - ), - &mut BinaryStringBitPerturbation::new(0.3), - &DefaultBetterThan::new(), - &BinaryString::new(vec![1; 10]), - ).unwrap(); - - println!("{:?}", result); - - assert_eq!( - result.best_candidate.fit, - 0.0 - ); - - assert_eq!( - result.best_candidate.pos, - optimum - ); -} - -#[test] -fn test_perturb() { - let mut rng = rand::rng(); - - assert_eq!( - BinaryString::new(vec![1, 1, 0, 0]) - .perturb(&mut rng, 1.0) - .vec, - vec![0, 0, 1, 1] - ); - - assert_eq!( - BinaryString::new(vec![1, 1, 0, 0]) - .perturb(&mut rng, 0.0) - .vec, - vec![1, 1, 0, 0] - ); -} - -#[test] -fn test_binary_string_to_real_single() { - assert_eq!( - BinaryString::new(vec![1]) - .to_real_single(0.0, 32.0), - 32.0 - ); - assert_eq!( - BinaryString::new(vec![1, 1]) - .to_real_single(0.0, 32.0), - 32.0 - ); - assert_eq!( - BinaryString::new(vec![0, 1]) - .to_real_single(0.0, 32.0), - 32.0 / 3.0 - ); - assert_eq!( - BinaryString::new(vec![0, 0]) - .to_real_single(-16.0, 16.0), - -16.0 - ); - assert_eq!( - BinaryString::new(vec![0, 0, 0, 0, 1]) - .to_real_single(0.0, 31.0), - 1.0 - ); - assert_eq!( - BinaryString::new(vec![1, 1, 1, 1, 1]) - .to_real_single(0.0, 31.0), - 31.0 - ); - assert_eq!( - BinaryString::new(vec![0, 0, 0, 1, 0]) - .to_real_single(0.0, 31.0), - 2.0 - ); - - assert_eq!( - BinaryString::new(vec![1; 512]) - .to_real_single(0.0, 31.0), - 31.0 - ); -} - -#[derive(Debug, Clone, PartialEq)] -struct DataArrOfReals { - vec: Vec, - valid: bool -} - -impl FromStr for DataArrOfReals { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - // TODO: maybe better handling, as an error? - // this would mean also reimplementing load_test_file to be able to supply - // out the error, but only for the output... - if !s.starts_with('[') { - return Ok( - DataArrOfReals { - valid: false, - vec: vec![] - } - ) - } - - let trimmed = &s[1..s.len()-1]; - - Ok(DataArrOfReals { - valid: true, - vec: trimmed.split(',') - .map(|x| x.trim().parse::()) - .collect::, _>>()? - }) - } -} - -fn test_binary_string_to_real(file_name: &str, bounds: Vec) { - let data = load_test_file::(file_name); - - for test in data { - let res = BinaryString::new(test.inp) - .to_real(&bounds); - if !test.out.valid { - assert_eq!( - res, - Err(BinaryStringConversionError::DimensionMismatch) - ); - } - else { - assert_eq!( - res.unwrap(), - test.out.vec - ); - } - } -} - -#[test] -fn test_binary_string_to_real_1D_1() { - test_binary_string_to_real( - "tests/Bin2Real_1D_1.txt", - vec![Bounds::new(0.0, 1.0)] - ); -} - -#[test] -fn test_binary_string_to_real_1D_2() { - test_binary_string_to_real( - "tests/Bin2Real_1D_2.txt", - vec![Bounds::new(0.0, 4095.0)] - ); -} - -#[test] -fn test_binary_string_to_real_1D_3() { - test_binary_string_to_real( - "tests/Bin2Real_1D_3.txt", - vec![Bounds::new(-5.0, 5.0)] - ); -} - -#[test] -fn test_binary_string_to_real_2D_1() { - test_binary_string_to_real( - "tests/Bin2Real_2D_1.txt", - vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] - ); -} - -#[test] -fn test_binary_string_to_real_2D_2() { - test_binary_string_to_real( - "tests/Bin2Real_2D_2.txt", - vec![Bounds::new(0.0, 63.0), Bounds::new(-32.0, 31.0)] - ); -} - -#[test] -fn test_binary_string_to_real_2D_3() { - test_binary_string_to_real( - "tests/Bin2Real_2D_3.txt", - vec![Bounds::new(-5.0, 5.0), Bounds::new(0.0, 10.0)] - ); -} - -#[test] -fn test_binary_string_to_real_3D_1() { - test_binary_string_to_real( - "tests/Bin2Real_3D_1.txt", - vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] - ); -} - -#[test] -fn test_binary_string_to_real_3D_2() { - test_binary_string_to_real( - "tests/Bin2Real_3D_2.txt", - vec![Bounds::new(0.0, 15.0), Bounds::new(-8.0, 7.0), Bounds::new(-8.0, 8.0)] - ); -} - -#[test] -fn test_binary_string_to_real_4D_1() { - test_binary_string_to_real( - "tests/Bin2Real_4D_1.txt", - vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] - ); -} - -#[test] -fn test_binary_string_to_real_4D_2() { - test_binary_string_to_real( - "tests/Bin2Real_4D_2.txt", - vec![Bounds::new(0.0, 7.0), Bounds::new(-4.0, 3.0), Bounds::new(-4.0, 4.0), Bounds::new(-8.0, 0.0)] - ); -} - -#[test] -fn test_binary_string_to_real_6D_1() { - test_binary_string_to_real( - "tests/Bin2Real_6D_1.txt", - vec![Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0), Bounds::new(0.0, 1.0)] - ); -} - -#[test] -fn test_binary_string_to_real_6D_2() { - test_binary_string_to_real( - "tests/Bin2Real_6D_2.txt", - (0..=5).map(|x| Bounds::new(x as f64, (2 * (x + 1)) as f64)).collect::>() - ); -} - -#[test] -fn test_one_max() { - let data = load_test_file::("tests/onemax.txt"); - - for test in data { - assert_eq!( - OneMax::new().fit(&BinaryString::new(test.inp)).unwrap(), - test.out - ); - } -} - -#[test] -fn test_LABS() { - let data = load_test_file::("tests/labs.txt"); - - for test in data { - println!("Test vector {}", test.inp.iter() - .map(|x| x.to_string()) - .collect::>() - .join(", ")); - assert_eq!( - LABS::new().fit(&BinaryString::new(test.inp)).unwrap(), - test.out - ) - } -} - -#[test] -fn test_sphere() { - let data = load_test_file::("tests/sphere.txt"); - - for test in data { - assert_eq!( - Sphere::new(vec![1.0; 10]).fit(&test.inp).unwrap(), - test.out - ) - } -} - -#[test] -fn test_rosenbrock() { - let data = load_test_file::("tests/rosenbrock.txt"); - - for test in data { - assert_eq!( - Rosenbrock::new().fit(&test.inp).unwrap(), - test.out - ) - } -} - -// Test infra -struct TestVector { - inp: Vec, - out: TOut -} - -fn load_test_file(file: &str) -> Vec> -where - TIn : FromStr + Debug, - TIn::Err: Debug, - TOut : FromStr + Debug, - TOut::Err: Debug, -{ - let mut vectors: Vec> = vec![]; - - let file = File::open(file).expect("Could not read test data!"); - let reader = BufReader::new(file); - - for (_, line) in reader.lines().enumerate() { - let line = line.expect("Could not read a line!"); - - if line.starts_with('#') || line.len() == 0 { - continue; - } - - let (inp_str, out_str) = line.split_once(":").unwrap(); - - let out: TOut = out_str.trim().parse::().unwrap(); - let inp: Vec = inp_str.split(' ') - .filter(|num| num.len() > 0) - .map(|num| num.trim().parse().unwrap()) - .collect(); - - vectors.push(TestVector:: { - inp, - out - }); - } - - vectors -} +mod test_infra; fn main() { - println!("Hello, world! {}", add(1, 2)); + println!("Hello, world!"); } diff --git a/env/src/perturbation/mod.rs b/env/src/perturbation/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..53143155cedf4dbfb474a63a94372a34f6c4e4ea --- /dev/null +++ b/env/src/perturbation/mod.rs @@ -0,0 +1,50 @@ +use rand::Rng; + +use crate::binary_string::BinaryString; + +pub trait PerturbationOperator { + type Chromosome; + + fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome; +} + +pub struct BinaryStringBitPerturbation { + rng: TRng, + p: f64, +} + +impl BinaryStringBitPerturbation { + pub fn new(p: f64) -> Self { + Self { + rng: rand::rng(), + p + } + } +} + +impl PerturbationOperator for BinaryStringBitPerturbation { + type Chromosome = BinaryString; + + fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome { + chromosome.perturb(&mut self.rng, self.p) + } +} + +#[test] +fn test_perturb() { + let mut rng = rand::rng(); + + assert_eq!( + *BinaryString::new(vec![1, 1, 0, 0]) + .perturb(&mut rng, 1.0) + .vec(), + vec![0, 0, 1, 1] + ); + + assert_eq!( + *BinaryString::new(vec![1, 1, 0, 0]) + .perturb(&mut rng, 0.0) + .vec(), + vec![1, 1, 0, 0] + ); +} diff --git a/env/src/terminating/mod.rs b/env/src/terminating/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8af4a594ac925172cb65c816b2f703f810ff29f --- /dev/null +++ b/env/src/terminating/mod.rs @@ -0,0 +1,106 @@ +use crate::{binary_string::BinaryString, local_search::LocalSearchCandidate}; + +pub trait TerminatingCondition + where T: Clone +{ + fn should_terminate( + self: &mut Self, + candidate: &LocalSearchCandidate, + stats: &Vec>, + cycle: usize + ) -> bool; +} + +pub struct EqualTerminatingCondition { + target: BinaryString, + remember_match: bool, + matched: bool, +} + +impl EqualTerminatingCondition { + pub fn new(target: BinaryString) -> Self { + Self { + target, + remember_match: false, + matched: false, + } + } + + pub fn new_remembered(target: BinaryString) -> Self { + Self { + target, + remember_match: true, + matched: false, + } + } + + pub fn reset_match(self: &mut Self) { + self.matched = false; + } +} + +impl TerminatingCondition for EqualTerminatingCondition { + fn should_terminate( + self: &mut Self, + candidate: &LocalSearchCandidate, + _: &Vec>, + _: usize + ) -> bool { + let matched = candidate.pos == self.target; + + if matched && self.remember_match { + self.matched = true; + } + + matched || self.matched + } +} + +pub struct NoBetterForCyclesTerminatingCondition { + cycles: usize +} + +impl NoBetterForCyclesTerminatingCondition { + pub fn new(cycles: usize) -> Self { + Self { + cycles + } + } +} + +pub struct AndTerminatingConditions<'a, T: Clone> { + terminating_conditions: Vec<&'a mut dyn TerminatingCondition> +} + +impl<'a, T: Clone> AndTerminatingConditions<'a, T> { + pub fn new(terminating_conditions: Vec<&'a mut dyn TerminatingCondition>) -> Self { + Self { + terminating_conditions + } + } +} + +impl<'a, T: Clone> TerminatingCondition for AndTerminatingConditions<'a, T> { + fn should_terminate( + self: &mut Self, + candidate: &LocalSearchCandidate, + stats: &Vec>, + cycle: usize + ) -> bool { + return self.terminating_conditions.iter_mut() + .all( + |cond| cond.should_terminate(candidate, stats, cycle) + ) + } +} + +impl TerminatingCondition for NoBetterForCyclesTerminatingCondition { + fn should_terminate ( + self: &mut Self, + candidate: &LocalSearchCandidate, + _: &Vec>, + cycle: usize + ) -> bool { + (cycle - candidate.cycle) > self.cycles + } +} diff --git a/env/src/test_infra.rs b/env/src/test_infra.rs new file mode 100644 index 0000000000000000000000000000000000000000..4e0563cf48c0b2511f78f975191b6a1deef29418 --- /dev/null +++ b/env/src/test_infra.rs @@ -0,0 +1,78 @@ +use std::fs::File; +use std::io::{BufRead,BufReader}; +use std::str::FromStr; +use std::fmt::Debug; + +pub struct TestVector { + pub inp: Vec, + pub out: TOut +} + +pub fn load_test_file(file: &str) -> Vec> +where + TIn : FromStr + Debug, + TIn::Err: Debug, + TOut : FromStr + Debug, + TOut::Err: Debug, +{ + let mut vectors: Vec> = vec![]; + + let file = File::open(file).expect("Could not read test data!"); + let reader = BufReader::new(file); + + for (_, line) in reader.lines().enumerate() { + let line = line.expect("Could not read a line!"); + + if line.starts_with('#') || line.len() == 0 { + continue; + } + + let (inp_str, out_str) = line.split_once(":").unwrap(); + + let out: TOut = out_str.trim().parse::().unwrap(); + let inp: Vec = inp_str.split(' ') + .filter(|num| num.len() > 0) + .map(|num| num.trim().parse().unwrap()) + .collect(); + + vectors.push(TestVector:: { + inp, + out + }); + } + + vectors +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DataArrOfReals { + pub vec: Vec, + pub valid: bool +} + +impl FromStr for DataArrOfReals { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + // TODO: maybe better handling, as an error? + // this would mean also reimplementing load_test_file to be able to supply + // out the error, but only for the output... + if !s.starts_with('[') { + return Ok( + DataArrOfReals { + valid: false, + vec: vec![] + } + ) + } + + let trimmed = &s[1..s.len()-1]; + + Ok(DataArrOfReals { + valid: true, + vec: trimmed.split(',') + .map(|x| x.trim().parse::()) + .collect::, _>>()? + }) + } +}