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::neighbour::Neighbour;
use crate::operators::mutation::Mutation;

/// This strategy will randomly change an existing gene to a new value that is guaranteed to be no
/// further away than `maximum_distance`. Like in all other strategies, random chance can also mean,
/// that the new gene will not be different from the previous gene. A typical case might be that a
/// gene is at the maximum value of its type (like [`u32::MAX`]) and the strategy wants to nudge in the
/// forward direction. There is no value available in that direction.
///
/// However, if the [Gene] type implements [Neighbour] in such a way that circular neighbour relations
/// are possible, it may always find a new gene.
///
/// # 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, PartialEq)]
// We want to configure and construct these structs directly for convenience.
#[allow(clippy::exhaustive_structs)]
pub struct Nudge {
    /// This value describes the biggest distance a new gene is allowed to be "away" from what was
    /// in its place before mutation. For more information check the [Neighbour] trait or specific
    /// implementations, depending on what type of genes are being mutated.
    pub maximum_distance: NonZeroUsize,
    /// A value in the range of `[0.0, 0.1]` describing how likely a random mutation will occur.
    pub chance: f64,
}

impl<G, const GENES: usize, RNG> Mutation<[G; GENES], RNG> for Nudge
where
    G: Gene + Neighbour,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut [G; GENES], rng: &mut RNG) {
        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        if rng.gen_bool(self.chance) {
            let random_slot = rng.gen_range(0..GENES);
            let selected_gene = &mut genotype[random_slot];

            let possible_new_gene = if rng.gen() {
                // we want a forward gene
                selected_gene.random_forward_neighbour(rng, self.maximum_distance)
            } else {
                // we want a backward gene
                selected_gene.random_backward_neighbour(rng, self.maximum_distance)
            };

            if let Some(new_gene) = possible_new_gene {
                *selected_gene = new_gene;
            }
        }
    }
}

impl<G, RNG> Mutation<Vec<G>, RNG> for Nudge
where
    G: Gene + Neighbour,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut Vec<G>, rng: &mut RNG) {
        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        if rng.gen_bool(self.chance) {
            let random_slot = rng.gen_range(0..genotype.len());
            let selected_gene = &mut genotype[random_slot];

            let possible_new_gene = if rng.gen() {
                // we want a forward gene
                selected_gene.random_forward_neighbour(rng, self.maximum_distance)
            } else {
                // we want a backward gene
                selected_gene.random_backward_neighbour(rng, self.maximum_distance)
            };

            if let Some(new_gene) = possible_new_gene {
                *selected_gene = new_gene;
            }
        }
    }
}

impl<RNG> Mutation<String, RNG> for Nudge
where
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut String, rng: &mut RNG) {
        // this is okay because we're indexing with a value that can't possibly be outside of the slice range
        #[allow(clippy::indexing_slicing)]
        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<_>>();

            let random_slot = rng.gen_range(0..chars.len());
            let selected_gene = &mut chars[random_slot];

            let possible_new_gene = if rng.gen() {
                // we want a forward gene
                selected_gene.random_forward_neighbour(rng, self.maximum_distance)
            } else {
                // we want a backward gene
                selected_gene.random_backward_neighbour(rng, self.maximum_distance)
            };

            if let Some(new_gene) = possible_new_gene {
                *selected_gene = new_gene;
            }

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

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

    use super::{Mutation, Nudge};

    #[test]
    fn array() {
        let mutation = Nudge {
            maximum_distance: 1.try_into().unwrap(),
            chance: 1.0_f64,
        };
        let mut rng = SmallRng::seed_from_u64(0);

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

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

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

    #[test]
    fn vector() {
        let mutation = Nudge {
            maximum_distance: 1.try_into().unwrap(),
            chance: 1.0_f64,
        };
        let mut rng = SmallRng::seed_from_u64(0);

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

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

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

    #[test]
    fn string() {
        let mutation = Nudge {
            maximum_distance: 1.try_into().unwrap(),
            chance: 1.0_f64,
        };
        let mut rng = SmallRng::seed_from_u64(4);

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

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

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