meiosis 0.1.0

An evolutionary algorithm library with as many compile time checks as possible.
Documentation
#![allow(box_pointers)]

use core::marker::PhantomData;

use rand::seq::SliceRandom;
use rand::Rng;

use crate::genotype::Genotype;

use super::Mutation;

/// For Rust beginners, this module may look a bit intimidating. All of these generic parameters
/// are necessary to verify valid mutation combinations at compile time.

/// With this combination strategy all mutation strategies get triggered in a random order.
/// Given that only one mutation can run at a time, depending on the order, the mutations could
/// potentionally influence each other. For example [Add] could execute and add a random gene,
/// after which [Remove] might execute and remove that exact gene again.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub struct RandomOrder;

/// With this combination strategy, each mutation has an equal chance of being selected.
/// Once one has been selected, it is triggered and no other mutation will be run.
/// Should the executed mutation have its own probability, it might or might not execute.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub struct Selective;

/// This mutation strategy allows combination of various other mutation strategies, including itself, for nesting.
///
/// # Examples
/// The following example demonstrates, that it is impossible to add
/// mutations to the [Combination], that are not implemented by the [Genotype].
#[allow(missing_debug_implementations)] // We can't do so because we can not guarantee `Mutation` to implement it.
pub struct Combination<S, G, RNG, const STRATEGIES: usize>
where
    G: Genotype,
    RNG: Rng,
{
    /// MISSING DOCS
    strategy: PhantomData<S>,
    /// MISSING DOCS
    rng: PhantomData<RNG>,
    /// MISSING DOCS
    genotype: PhantomData<G>,
    /// MISSING DOCS
    strategies: [Box<dyn Mutation<G, RNG>>; STRATEGIES],
}

impl<G, RNG> Combination<RandomOrder, G, RNG, 0>
where
    G: Genotype,
    RNG: Rng,
{
    /// Creates a new [Combination] using the [`RandomOrder`] strategy.
    #[must_use]
    pub fn random() -> Combination<RandomOrder, G, RNG, 0> {
        Combination {
            strategy: PhantomData::<RandomOrder>,
            rng: PhantomData::<RNG>,
            genotype: PhantomData::<G>,
            strategies: [],
        }
    }
}

impl<G, RNG> Combination<Selective, G, RNG, 0>
where
    G: Genotype,
    RNG: Rng,
{
    /// Creates a new [Combination] using the [Selective] strategy.
    #[must_use]
    pub fn selective() -> Combination<Selective, G, RNG, 0> {
        Combination {
            strategy: PhantomData::<Selective>,
            rng: PhantomData::<RNG>,
            genotype: PhantomData::<G>,
            strategies: [],
        }
    }
}

impl<S, G, RNG, const STRATEGIES: usize> Combination<S, G, RNG, STRATEGIES>
where
    G: Genotype,
    RNG: Rng,
{
    /// MISSING DOCS
    pub fn and<M>(self, mutation: M) -> Combination<S, G, RNG, { STRATEGIES + 1 }>
    where
        M: Mutation<G, RNG> + 'static,
    {
        let boxed: Box<dyn Mutation<G, RNG>> = Box::new(mutation);

        // construct an iterator from existing elements and a single new one (`Option` implements `Iterator`)
        let mut iter = self.strategies.into_iter().chain(Some(boxed));
        // create array with iterator results
        // this can't fail as lengths are known exactly and always match.
        // `from_fn` can infer the array length from the return type
        #[allow(clippy::expect_used)]
        let strategies = core::array::from_fn(|_| iter.next().expect("constructed iterator has known length"));

        Combination {
            strategy: self.strategy,
            rng: self.rng,
            genotype: self.genotype,
            strategies,
        }
    }
}

impl<G, RNG, const STRATEGIES: usize> Mutation<G, RNG> for Combination<RandomOrder, G, RNG, STRATEGIES>
where
    G: Genotype,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut G, rng: &mut RNG) {
        // replace this with `{ STRATEGIES > 0 }` the moment it becomes available in nightly,
        // so that we can guarantee this at compile time
        assert!(STRATEGIES > 0, "combination mutation should contain strategies");

        for strategy in self.strategies.choose_multiple(rng, STRATEGIES) {
            strategy.mutate(genotype, rng);
        }
    }
}

impl<G, RNG, const STRATEGIES: usize> Mutation<G, RNG> for Combination<Selective, G, RNG, STRATEGIES>
where
    G: Genotype,
    RNG: Rng,
{
    fn mutate(&self, genotype: &mut G, rng: &mut RNG) {
        // replace this with `{ STRATEGIES > 0 }` the moment it becomes available in nightly,
        // so that we can guarantee this at compile time
        assert!(STRATEGIES > 0, "combination mutation should contain strategies");

        #[allow(clippy::expect_used)]
        let strategy = self
            .strategies
            .choose(rng)
            .expect("previous assert makes this always return some");

        strategy.mutate(genotype, rng);
    }
}

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

    use crate::operators::mutation::{add::Add, random::single::uniform::Uniform, remove::Remove, swap::Swap};

    use super::{Combination, Mutation};

    #[test]
    fn random() {
        // we set mutation chance to 1 to guarantee it
        let add = Add {
            chance: 1.0_f64,
            max_genes: None,
        };

        let random = Uniform { chance: 1.0 };

        let combination = Combination::random().and(add).and(random);
        let mut rng = SmallRng::seed_from_u64(1);

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

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

        // 229 was added
        // 2 mutated to 237
        assert_eq!(array, [229, 0, 1, 237, 3, 4]);
    }

    #[test]
    fn selective() {
        // we set mutation chance to 1 to guarantee it
        let swap = Swap { chance: 1.0 };

        let remove = Remove {
            chance: 1.0_f64,
            min_genes: 1.try_into().unwrap(),
        };

        let combination = Combination::selective().and(swap).and(remove);
        let mut rng = SmallRng::seed_from_u64(0);

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

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

        // only the remove mutation ran and removed 2
        assert_eq!(array, [0, 1, 3, 4]);
    }
}