~ruther/ctu-fee-eoa

dfddcfde9cade6fd94ace5bca2f846f48c230e3c — Rutherther a day ago 2d3672a
chore: split types and functions to separate module files
A env/src/binary_string.rs => env/src/binary_string.rs +285 -0
@@ 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<i8>
}

#[derive(Debug, Clone, PartialEq)]
pub enum BinaryStringConversionError {
    DimensionMismatch,
    NoBounds
}

impl BinaryString {
    pub fn new(vec: Vec<i8>) -> BinaryString {
        BinaryString {
            vec
        }
    }

    pub fn vec(&self) -> &Vec<i8> {
        &self.vec
    }

    pub fn perturb<TRng>(self: &Self, rng: &mut TRng, p: f64) -> Self
        where TRng : Rng
    {
        BinaryString::new(
            self.into_iter()
                .map(|c| if rng.random::<f64>() <= p { 1 - *c } else { *c })
                .collect::<Vec<i8>>()
        )
    }

    fn to_real_internal<'a, T: DoubleEndedIterator<Item = &'a i8>>(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::<f64>();

        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<Bounds>) -> Result<Vec<f64>, 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::<Vec<f64>>())
    }
}

impl FromStr for BinaryString {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let binary_vec: Vec<i8> = 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::<Result<Vec<i8>, 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<Bounds>) {
    let data = load_test_file::<i8, DataArrOfReals>(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::<Vec<_>>()
    );
}

A env/src/comparison/mod.rs => env/src/comparison/mod.rs +18 -0
@@ 0,0 1,18 @@
pub trait BetterThanOperator<T> {
    fn better_than(self: &Self, a: &T, b: &T) -> bool;
}

pub struct MinimizingOperator;
impl MinimizingOperator {
    pub fn new() -> Self {
        Self
    }
}

impl<T> BetterThanOperator<T> for MinimizingOperator
    where T: PartialOrd
{
    fn better_than(self: &Self, a: &T, b: &T) -> bool {
        a < b
    }
}

A env/src/fitness/labs.rs => env/src/fitness/labs.rs +54 -0
@@ 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>) -> 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<i32, Infallible> {
        let S: Vec<i32> = 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::<i8, i32>("tests/labs.txt");

    for test in data {
        println!("Test vector {}", test.inp.iter()
            .map(|x| x.to_string())
            .collect::<Vec<String>>()
            .join(", "));
        assert_eq!(
            LABS::new().fit(&BinaryString::new(test.inp)).unwrap(),
            test.out
        )
     }
}

A env/src/fitness/mod.rs => env/src/fitness/mod.rs +43 -0
@@ 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<Self::Out, Self::Err>;
}

pub struct BinaryFitnessWrapper<TFitness> {
    bounds: Vec<Bounds>,
    fitting_function: TFitness,
}

impl<TFitness> BinaryFitnessWrapper<TFitness> {
    pub fn new(fitting_function: TFitness, bounds: Vec<Bounds>) -> Self {
        BinaryFitnessWrapper {
            fitting_function,
            bounds,
        }
    }
}

impl<TFitness> FitnessFunction for BinaryFitnessWrapper<TFitness>
where
    TFitness: FitnessFunction<In = Vec<f64>, Out = f64, Err = Infallible>
{
    type In = BinaryString;
    type Out = f64;
    type Err = BinaryStringConversionError;

    fn fit(self: &Self, inp: &BinaryString) -> Result<f64, BinaryStringConversionError> {
        Ok(self.fitting_function.fit(&inp.to_real(&self.bounds)?).unwrap())
    }
}

A env/src/fitness/one_max.rs => env/src/fitness/one_max.rs +36 -0
@@ 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<i32, Infallible> {
        Ok(chromosome.into_iter()
           .map(|x| *x as i32)
           .sum())
    }
}

#[test]
fn test_one_max() {
    let data = load_test_file::<i8, i32>("tests/onemax.txt");

    for test in data {
        assert_eq!(
            OneMax::new().fit(&BinaryString::new(test.inp)).unwrap(),
            test.out
        );
    }
}

A env/src/fitness/rosenbrock.rs => env/src/fitness/rosenbrock.rs +36 -0
@@ 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<f64>;
    type Out = f64;
    type Err = Infallible;

    fn fit(self: &Self, inp: &Vec<f64>) -> Result<f64, Infallible> {
        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::<f64, f64>("tests/rosenbrock.txt");

    for test in data {
        assert_eq!(
            Rosenbrock::new().fit(&test.inp).unwrap(),
            test.out
        )
    }
}

A env/src/fitness/sphere.rs => env/src/fitness/sphere.rs +42 -0
@@ 0,0 1,42 @@
use std::convert::Infallible;
use crate::test_infra::load_test_file;

use super::FitnessFunction;

pub struct Sphere {
    offset: Vec<f64>
}

impl Sphere {
    pub fn new(offset: Vec<f64>) -> Self {
        Sphere {
            offset
        }
    }
}

impl FitnessFunction for Sphere {
    type In = Vec<f64>;
    type Out = f64;
    type Err = Infallible;

    fn fit(self: &Self, chromosome: &Vec<f64>) -> Result<f64, Infallible> {
        Ok(chromosome
           .iter()
           .zip(&self.offset)
           .map(|(x, o)| (x - o).powi(2))
           .sum())
    }
}

#[test]
fn test_sphere() {
    let data = load_test_file::<f64, f64>("tests/sphere.txt");

    for test in data {
        assert_eq!(
            Sphere::new(vec![1.0; 10]).fit(&test.inp).unwrap(),
            test.out
        )
    }
}

A env/src/local_search/mod.rs => env/src/local_search/mod.rs +147 -0
@@ 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<T>
    where T: Clone
{
    pub fit: T,
    pub pos: BinaryString,
    pub cycle: usize
}

#[derive(Debug, Clone, PartialEq)]
pub struct LocalSearchResult<T>
    where T: Clone
{
    pub best_candidate: LocalSearchCandidate<T>,

    // How many cycles there were
    pub cycles: usize,

    // statistics
    pub best_candidates: Vec<LocalSearchCandidate<T>>
}

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<LocalSearchResult<T>, TErr>
where
    T: Clone,
    TFit: FitnessFunction<In = BinaryString, Out = T, Err = TErr>,
    TTerminatingCondition: TerminatingCondition<T>,
    TPerturbationOperator: PerturbationOperator<Chromosome = BinaryString>,
    TBetterThanOperator: BetterThanOperator<T>,
{
    let mut best_candidate = LocalSearchCandidate {
        pos: initial.clone(),
        fit: fit.fit(&initial)?,
        cycle: 0
    };

    let mut stats: Vec<LocalSearchCandidate<T>> = 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
    );
}

M env/src/main.rs => env/src/main.rs +8 -844
@@ 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<i8>
}

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<i8>) -> BinaryString {
        BinaryString {
            vec
        }
    }

    pub fn perturb<TRng>(self: &Self, rng: &mut TRng, p: f64) -> Self
        where TRng : Rng
    {
        BinaryString::new(
            self.into_iter()
                .map(|c| if rng.random::<f64>() <= p { 1 - *c } else { *c })
                .collect::<Vec<i8>>()
        )
    }

    fn to_real_internal<'a, T: DoubleEndedIterator<Item = &'a i8>>(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::<f64>();

        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<Bounds>) -> Result<Vec<f64>, 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::<Vec<f64>>())
    }
}

impl FromStr for BinaryString {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let binary_vec: Vec<i8> = 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::<Result<Vec<i8>, 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<Self::Out, Self::Err>;
}

// 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<i32, Infallible> {
        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>) -> 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<i32, Infallible> {
        let S: Vec<i32> = 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<f64>
}

impl Sphere {
    pub fn new(offset: Vec<f64>) -> Self {
        Sphere {
            offset
        }
    }
}
impl FittingFunction for Sphere {
    type In = Vec<f64>;
    type Out = f64;
    type Err = Infallible;

    fn fit(self: &Self, chromosome: &Vec<f64>) -> Result<f64, Infallible> {
        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<f64>;
    type Out = f64;
    type Err = Infallible;

    fn fit(self: &Self, inp: &Vec<f64>) -> Result<f64, Infallible> {
        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<TFitting> {
    bounds: Vec<Bounds>,
    fitting_function: TFitting,
}

impl<TFitting> BinaryFittingWrapper<TFitting> {
    pub fn new(fitting_function: TFitting, bounds: Vec<Bounds>) -> Self {
        BinaryFittingWrapper {
            fitting_function,
            bounds,
        }
    }
}

impl<TFitting> FittingFunction for BinaryFittingWrapper<TFitting>
where
    TFitting: FittingFunction<In = Vec<f64>, Out = f64, Err = Infallible>
{
    type In = BinaryString;
    type Out = f64;
    type Err = BinaryStringConversionError;

    fn fit(self: &Self, inp: &BinaryString) -> Result<f64, BinaryStringConversionError> {
        Ok(self.fitting_function.fit(&inp.to_real(&self.bounds)?).unwrap())
    }
}

#[derive(Debug, Clone, PartialEq)]
struct LocalSearchCandidate<T>
    where T: Clone
{
    fit: T,
    pos: BinaryString,
    cycle: usize
}

#[derive(Debug, Clone, PartialEq)]
struct LocalSearchResult<T>
    where T: Clone
{
    best_candidate: LocalSearchCandidate<T>,

    // How many cycles there were
    cycles: usize,

    // statistics
    best_candidates: Vec<LocalSearchCandidate<T>>
}

trait TerminatingCondition<T>
    where T: Clone
{
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        stats: &Vec<LocalSearchCandidate<T>>,
        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<T: Clone> TerminatingCondition<T> for EqualTerminatingCondition {
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        _: &Vec<LocalSearchCandidate<T>>,
        _: 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<T>>
}

impl<'a, T: Clone> AndTerminatingConditions<'a, T> {
    pub fn new(terminating_conditions: Vec<&'a mut dyn TerminatingCondition<T>>) -> Self {
        Self {
            terminating_conditions
        }
    }
}

impl<'a, T: Clone> TerminatingCondition<T> for AndTerminatingConditions<'a, T> {
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        stats: &Vec<LocalSearchCandidate<T>>,
        cycle: usize
    ) -> bool {
        return self.terminating_conditions.iter_mut()
            .all(
                |cond| cond.should_terminate(candidate, stats, cycle)
            )
    }
}

impl<T: Clone> TerminatingCondition<T> for NoBetterForCyclesTerminatingCondition {
    fn should_terminate (
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        _: &Vec<LocalSearchCandidate<T>>,
        cycle: usize
    ) -> bool {
        (cycle - candidate.cycle) > self.cycles
    }
}

trait PerturbationOperator {
    type Chromosome;

    fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome;
}

struct BinaryStringBitPerturbation<TRng: Rng> {
    rng: TRng,
    p: f64,
}

impl BinaryStringBitPerturbation<rand::rngs::ThreadRng> {
    pub fn new(p: f64) -> Self {
        Self {
            rng: rand::rng(),
            p
        }
    }
}

impl<TRng: Rng> PerturbationOperator for BinaryStringBitPerturbation<TRng> {
    type Chromosome = BinaryString;

    fn perturb(self: &mut Self, chromosome: &Self::Chromosome) -> Self::Chromosome {
        chromosome.perturb(&mut self.rng, self.p)
    }
}

trait BetterThanOperator<T> {
    fn better_than(self: &Self, a: &T, b: &T) -> bool;
}

struct DefaultBetterThan;
impl DefaultBetterThan {
    pub fn new() -> Self {
        Self
    }
}

impl<T> BetterThanOperator<T> 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<LocalSearchResult<T>, TErr>
where
    T: Clone,
    TFit: FittingFunction<In = BinaryString, Out = T, Err = TErr>,
    TTerminatingCondition: TerminatingCondition<T>,
    TPerturbationOperator: PerturbationOperator<Chromosome = BinaryString>,
    TBetterThanOperator: BetterThanOperator<T>,
{
    let mut best_candidate = LocalSearchCandidate {
        pos: initial.clone(),
        fit: fit.fit(&initial)?,
        cycle: 0
    };

    let mut stats: Vec<LocalSearchCandidate<T>> = 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<f64>,
    valid: bool
}

impl FromStr for DataArrOfReals {
    type Err = <f64 as FromStr>::Err;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // 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::<f64>())
                .collect::<Result<Vec<f64>, _>>()?
        })
    }
}

fn test_binary_string_to_real(file_name: &str, bounds: Vec<Bounds>) {
    let data = load_test_file::<i8, DataArrOfReals>(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::<Vec<_>>()
    );
}

#[test]
fn test_one_max() {
    let data = load_test_file::<i8, i32>("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::<i8, i32>("tests/labs.txt");

    for test in data {
        println!("Test vector {}", test.inp.iter()
            .map(|x| x.to_string())
            .collect::<Vec<String>>()
            .join(", "));
        assert_eq!(
            LABS::new().fit(&BinaryString::new(test.inp)).unwrap(),
            test.out
        )
     }
}

#[test]
fn test_sphere() {
    let data = load_test_file::<f64, f64>("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::<f64, f64>("tests/rosenbrock.txt");

    for test in data {
        assert_eq!(
            Rosenbrock::new().fit(&test.inp).unwrap(),
            test.out
        )
    }
}

// Test infra
struct TestVector<TIn, TOut> {
    inp: Vec<TIn>,
    out: TOut
}

fn load_test_file<TIn, TOut>(file: &str) -> Vec<TestVector::<TIn, TOut>>
where
  TIn : FromStr + Debug,
  TIn::Err: Debug,
  TOut : FromStr + Debug,
  TOut::Err: Debug,
{
    let mut vectors: Vec<TestVector::<TIn, TOut>> = 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::<TOut>().unwrap();
        let inp: Vec<TIn> = inp_str.split(' ')
            .filter(|num| num.len() > 0)
            .map(|num| num.trim().parse().unwrap())
            .collect();

        vectors.push(TestVector::<TIn, TOut> {
            inp,
            out
        });
    }

    vectors
}
mod test_infra;

fn main() {
    println!("Hello, world! {}", add(1, 2));
    println!("Hello, world!");
}

A env/src/perturbation/mod.rs => env/src/perturbation/mod.rs +50 -0
@@ 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<TRng: Rng> {
    rng: TRng,
    p: f64,
}

impl BinaryStringBitPerturbation<rand::rngs::ThreadRng> {
    pub fn new(p: f64) -> Self {
        Self {
            rng: rand::rng(),
            p
        }
    }
}

impl<TRng: Rng> PerturbationOperator for BinaryStringBitPerturbation<TRng> {
    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]
    );
}

A env/src/terminating/mod.rs => env/src/terminating/mod.rs +106 -0
@@ 0,0 1,106 @@
use crate::{binary_string::BinaryString, local_search::LocalSearchCandidate};

pub trait TerminatingCondition<T>
    where T: Clone
{
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        stats: &Vec<LocalSearchCandidate<T>>,
        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<T: Clone> TerminatingCondition<T> for EqualTerminatingCondition {
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        _: &Vec<LocalSearchCandidate<T>>,
        _: 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<T>>
}

impl<'a, T: Clone> AndTerminatingConditions<'a, T> {
    pub fn new(terminating_conditions: Vec<&'a mut dyn TerminatingCondition<T>>) -> Self {
        Self {
            terminating_conditions
        }
    }
}

impl<'a, T: Clone> TerminatingCondition<T> for AndTerminatingConditions<'a, T> {
    fn should_terminate(
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        stats: &Vec<LocalSearchCandidate<T>>,
        cycle: usize
    ) -> bool {
        return self.terminating_conditions.iter_mut()
            .all(
                |cond| cond.should_terminate(candidate, stats, cycle)
            )
    }
}

impl<T: Clone> TerminatingCondition<T> for NoBetterForCyclesTerminatingCondition {
    fn should_terminate (
        self: &mut Self,
        candidate: &LocalSearchCandidate<T>,
        _: &Vec<LocalSearchCandidate<T>>,
        cycle: usize
    ) -> bool {
        (cycle - candidate.cycle) > self.cycles
    }
}

A env/src/test_infra.rs => env/src/test_infra.rs +78 -0
@@ 0,0 1,78 @@
use std::fs::File;
use std::io::{BufRead,BufReader};
use std::str::FromStr;
use std::fmt::Debug;

pub struct TestVector<TIn, TOut> {
    pub inp: Vec<TIn>,
    pub out: TOut
}

pub fn load_test_file<TIn, TOut>(file: &str) -> Vec<TestVector::<TIn, TOut>>
where
  TIn : FromStr + Debug,
  TIn::Err: Debug,
  TOut : FromStr + Debug,
  TOut::Err: Debug,
{
    let mut vectors: Vec<TestVector::<TIn, TOut>> = 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::<TOut>().unwrap();
        let inp: Vec<TIn> = inp_str.split(' ')
            .filter(|num| num.len() > 0)
            .map(|num| num.trim().parse().unwrap())
            .collect();

        vectors.push(TestVector::<TIn, TOut> {
            inp,
            out
        });
    }

    vectors
}

#[derive(Debug, Clone, PartialEq)]
pub struct DataArrOfReals {
    pub vec: Vec<f64>,
    pub valid: bool
}

impl FromStr for DataArrOfReals {
    type Err = <f64 as FromStr>::Err;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // 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::<f64>())
                .collect::<Result<Vec<f64>, _>>()?
        })
    }
}