meiosis 0.1.0

An evolutionary algorithm library with as many compile time checks as possible.
Documentation
use core::num::NonZeroUsize;

use rand::Rng;

use crate::gene::Gene;
use crate::genotype::Genotype;
use crate::random::Random;

use super::Mutation;

/// This strategy extends the genome by adding a single random gene.
///
/// It is advised to check the trait implementation documentation for more details on how genes are added.
///
/// # 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 Add {
    /// A value in the range of `[0.0, 0.1]` describing how likely a random mutation will occur.
    pub chance: f64,
    /// If the `Genotype` we're mutating already has this many genes, the strategy will do nothing.
    pub max_genes: Option<NonZeroUsize>,
}

/// Mutation for [Vec] pushes new [Gene]s randomly into [Vec].
impl<G, RNG> Mutation<Vec<G>, RNG> for Add
where
    G: Gene + Random,
    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 let Some(max_genes) = self.max_genes {
            if genotype.len() >= max_genes.get() {
                return;
            }
        }

        if rng.gen_bool(self.chance) {
            let random_gene = G::random(rng);

            let position_for_new_gene = rng.gen_range(0..=genotype.len());
            genotype.insert(position_for_new_gene, random_gene);
        }
    }
}

/// Mutation for [String] pushes new [Gene]s randomly to the [String].
impl<RNG> Mutation<String, RNG> for Add
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 let Some(max_genes) = self.max_genes {
            if genotype.len() >= max_genes.get() {
                return;
            }
        }

        if rng.gen_bool(self.chance) {
            // Technically, we could simply call `char::random` here,
            // but this way correctly displays the relationship and hierarchy.
            let random_gene = <String as Genotype>::Gene::random(rng);

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

            let position_for_new_gene = rng.gen_range(0..=chars.len());
            chars.insert(position_for_new_gene, random_gene);

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

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

    use super::{Add, Mutation};

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

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

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

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

    #[test]
    fn vector_max_genes() {
        // we set mutation chance to 1 to guarantee it
        let mutation = Add {
            chance: 1.0_f64,
            max_genes: Some(5.try_into().unwrap()),
        };
        let mut rng = SmallRng::seed_from_u64(0);

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

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

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

    #[test]
    fn string() {
        // we set mutation chance to 1 to guarantee it
        let mutation = Add {
            chance: 1.0_f64,
            max_genes: None,
        };
        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}st");
    }
}