meiosis 0.1.0

An evolutionary algorithm library with as many compile time checks as possible.
Documentation
use rand::Rng;

use crate::gene::Gene;

use super::Mutation;

/// This strategy takes two random genes and swaps them.
///
/// Given that a [`crate::genotype::Genotype`] with just a single [Gene] is valid, implementations of this strategy
/// are encouraged to return early, should that be the case.
///
/// # Panics
/// A runtime check is performed to make sure the `chance` of mutation is in the valid range of `[0.0, 1.0]`.
#[derive(Copy, Clone, Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct Swap {
    /// A value in the range of `[0.0, 0.1]` describing how likely a random mutation will occur.
    ///
    /// It is very important to keep in mind that this is the mathematical chance of *triggering*
    /// a mutation, thereby swapping two genes with the same value is still valid.
    /// For an outside observer it looks as if the genome hasn't changed, but a mutation still occured.
    pub chance: f64,
}

impl<G, const GENES: usize, RNG> Mutation<[G; GENES], RNG> for Swap
where
    G: Gene,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut [G; GENES], rng: &mut RNG) {
        assert!(
            self.chance >= 0.0_f64,
            "chance of mutation has to be above or equal 0.0, but is {}",
            self.chance
        );
        assert!(
            self.chance <= 1.0_f64,
            "chance of mutation has to be below or equal 1.0, but is {}",
            self.chance
        );

        if genotype.len() < 2 {
            return;
        }

        if rng.gen_bool(self.chance) {
            generic_slice_swap(genotype, rng);
        }
    }
}

impl<G, RNG> Mutation<Vec<G>, RNG> for Swap
where
    G: Gene,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut Vec<G>, rng: &mut RNG) {
        assert!(
            self.chance >= 0.0_f64,
            "chance of mutation has to be above or equal 0.0, but is {}",
            self.chance
        );
        assert!(
            self.chance <= 1.0_f64,
            "chance of mutation has to be below or equal 1.0, but is {}",
            self.chance
        );

        if genotype.len() < 2 {
            return;
        }

        if rng.gen_bool(self.chance) {
            generic_slice_swap(genotype, rng);
        }
    }
}

impl<RNG> Mutation<String, RNG> for Swap
where
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut String, rng: &mut RNG) {
        assert!(
            self.chance >= 0.0_f64,
            "chance of mutation has to be above or equal 0.0, but is {}",
            self.chance
        );
        assert!(
            self.chance <= 1.0_f64,
            "chance of mutation has to be below or equal 1.0, but is {}",
            self.chance
        );

        if genotype.len() < 2 {
            return;
        }

        if rng.gen_bool(self.chance) {
            // Because of the UTF-8 nature of Strings, we can not manipulate `char`s in place,
            // as that may require resizing.
            // The next best solution is to collect all chars into a Vec, manipulate that, and
            // convert it back into a String.
            let mut chars = genotype.chars().collect::<Vec<_>>();

            generic_slice_swap(&mut chars, rng);

            *genotype = chars.iter().collect::<String>();
        }
    }
}

/// MISSING DOCS
fn generic_slice_swap<G, R>(genotype: &mut [G], rng: &mut R)
where
    R: Rng,
{
    if genotype.len() < 2 {
        return;
    }

    // we select two random points
    let slot_a = rng.gen_range(0..genotype.len());
    let slot_b = loop {
        let potential_slot = rng.gen_range(0..genotype.len());

        // Thanks to the assertion above, we know that at some point we have to get a
        // differing slot compared to `slot_a`, so we run this loop till that happens.
        // Not necessarily the best option, but it'll do for now.
        if potential_slot != slot_a {
            break potential_slot;
        }
    };

    genotype.swap(slot_a, slot_b);
}

#[cfg(test)]
mod tests {
    use rand::rngs::SmallRng;
    use rand::SeedableRng;

    use super::{Mutation, Swap};

    #[test]
    fn array() {
        // we set mutation chance to 1 to guarantee it
        let mutation = Swap { chance: 1.0 };
        let mut rng = SmallRng::seed_from_u64(1);

        let mut array = [0_u8, 1, 2, 3, 4];

        mutation.mutate(&mut array, &mut rng);

        assert_eq!(array, [3, 1, 2, 0, 4]);
    }

    #[test]
    fn vector() {
        // we set mutation chance to 1 to guarantee it
        let mutation = Swap { chance: 1.0 };
        let mut rng = SmallRng::seed_from_u64(1);

        let mut array = vec![0_u8, 1, 2, 3, 4];

        mutation.mutate(&mut array, &mut rng);

        assert_eq!(array, [3, 1, 2, 0, 4]);
    }

    #[test]
    fn string() {
        // we set mutation chance to 1 to guarantee it
        let mutation = Swap { chance: 1.0 };
        let mut rng = SmallRng::seed_from_u64(0);

        let mut string = String::from("test");

        mutation.mutate(&mut string, &mut rng);

        assert_eq!(string, "tets");
    }
}