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 crate::genotype::Genotype;
use crate::operators::mutation::Mutation;
use crate::random::Random as RandomGene;

use super::mutations_by_rate;

/// This strategy will randomly change multiple genes to new random values. It calculates the
/// amount of genes required according to the rate and then randomly selects that many genes.
/// It may end up choosing a single gene multiple times, so the rate is the upper limit.
/// # Panics
/// A runtime check is performed to make sure the `rate` of mutation is in the valid range of `[0.0, 1.0]`.
#[derive(Copy, Clone, Debug)]
// We want to configure and construct these structs directly for convenience.
#[allow(clippy::exhaustive_structs)]
pub struct Uniform {
    /// A value in the range of `[0.0, 0.1]` describing in percent how many genes will be mutated.
    pub rate: f64,
}

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

        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        for _ in 0..mutations_by_rate(GENES, self.rate) {
            let random_gene = G::random(rng);
            let random_slot = rng.gen_range(0..genotype.len());
            genotype[random_slot] = random_gene;
        }
    }
}

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

        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        for _ in 0..mutations_by_rate(genotype.len(), self.rate) {
            let random_gene = G::random(rng);
            let random_slot = rng.gen_range(0..genotype.len());
            genotype[random_slot] = random_gene;
        }
    }
}

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

        // 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<_>>();

        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        for _ in 0..mutations_by_rate(chars.len(), self.rate) {
            let random_gene = <String as Genotype>::Gene::random(rng);
            let random_slot = rng.gen_range(0..genotype.len());
            chars[random_slot] = random_gene;
        }

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

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

    use super::{Mutation, Uniform};

    #[test]
    fn array() {
        // we set mutation chance to 0.4 so 2 genes should mutate
        let mutation = Uniform { rate: 0.4 };
        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, [74, 1, 185, 3, 4]);
    }

    #[test]
    fn vector() {
        // we set mutation chance to 0.4 so 2 genes should mutate
        let mutation = Uniform { rate: 0.4 };
        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, [74, 1, 185, 3, 4]);
    }

    #[test]
    fn string() {
        // we set mutation chance to 0.25 so 1 gene should mutate
        let mutation = Uniform { rate: 0.25 };
        let mut rng = SmallRng::seed_from_u64(0);

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

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

        assert_eq!(string, "te\u{79f2e}t");
    }
}