meiosis 0.1.0

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

use rand::{thread_rng, Rng};

use meiosis::algorithm::fitness::Configuration;
use meiosis::environment::Environment;
use meiosis::fitness::Fitness;
use meiosis::operators::mutation::combination::Combination;
use meiosis::operators::mutation::{add::Add, random::multi::nudge::Nudge, remove::Remove, swap::Swap};
use meiosis::operators::recombination::single_point::SinglePoint;
use meiosis::operators::selection::fitness_proportionate::StochasticAcceptance;
use meiosis::phenotype::{FromGenotype, Phenotype};
use meiosis::termination::fitness::Fitness as FitnessTermination;

const TARGET: &str = "Hello, World!";
//const TARGET: &str = "Hello, I'm apparently very slow at developing!";

#[derive(Debug, Clone, PartialEq)]
struct EvolvedString(Vec<u8>);

impl Phenotype for EvolvedString {
    type Genotype = Vec<u8>;

    fn to_genotype(self) -> Self::Genotype {
        self.0
    }
}

struct EvolveHelloWorld {
    target: Vec<u8>,
}

impl FromGenotype<Vec<u8>, EvolveHelloWorld> for EvolvedString {
    fn from_genotype<RNG>(_rng: &mut RNG, genotype: Vec<u8>, _environment: &EvolveHelloWorld) -> Self
    where
        RNG: Rng,
    {
        EvolvedString(genotype)
    }
}

impl Environment for EvolveHelloWorld {}

/// Define how well [`EvolvedString`] does in an [`EvolveHelloWorld`] environment.
impl Fitness<EvolveHelloWorld> for EvolvedString {
    #[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
    fn fitness(&self, environment: &EvolveHelloWorld) -> f64 {
        let chars = &self.0;
        let target = &environment.target;

        let correct = chars
            .iter()
            .zip(target)
            .map(|(c, t)| c == t)
            .filter(|result| *result)
            .count();

        correct as f64 / self.0.len().max(environment.target.len()) as f64
    }
}

fn main() {
    // note that all chances are set to 1 because we selectively do only one mutation per mutation
    let mutation = Combination::selective()
        .and(Add {
            chance: 1.,
            max_genes: None,
        })
        .and(Remove {
            chance: 1.,
            min_genes: 5.try_into().unwrap(),
        })
        .and(Swap { chance: 1. })
        .and(Nudge {
            maximum_distance: 10.try_into().unwrap(),
            rate: 0.3,
        });

    let config = Configuration::new()
        // we invisibly switch to a configuration struct used to collect basic information
        .with_rng(thread_rng())
        .with_environment(EvolveHelloWorld {
            target: TARGET.as_bytes().to_vec(),
        })
        .with_selection(StochasticAcceptance::default())
        .with_recombination(SinglePoint {})
        .with_mutation(mutation)
        // we invisibly switch back to the algorithm, configuring the final details
        .with_termination(FitnessTermination(1.));

    // set up final runtime parameters
    let mut classic = config
        .with_elitism(2)
        .with_speciation_threshold(0.999)
        .with_interspecies_breeding_chance(0.005)
        .with_random_population(100);

    // we're manually iterating the algorithm so we can print out statistics
    let result = loop {
        classic = match classic.evaluate::<EvolvedString>().check_termination() {
            Ok(new_evaluated) => {
                let iteration = new_evaluated.statistics().iteration;

                if iteration % 100 == 0 {
                    let species = new_evaluated.state().species();

                    let population_count = species.iter().map(|s| s.population.members.len()).sum::<usize>();

                    println!(
                        "generation: {iteration}, population: {population_count}, species: {}",
                        species.len(),
                    );

                    for evaluated_species in species.iter() {
                        #[allow(clippy::expect_used)]
                        let fittest = evaluated_species
                            .population
                            .members
                            .iter()
                            .max_by(|a, b| a.fitness.total_cmp(&b.fitness))
                            .expect("iterator can not be empty");

                        println!(
                            "\tSpecies: {}-{}, members: {}, fitness: {}, best member: \"{}\"",
                            evaluated_species.identifier.0,
                            evaluated_species.identifier.1,
                            evaluated_species.population.members.len(),
                            fittest.fitness,
                            String::from_utf8_lossy(&fittest.phenotype.0),
                        );
                    }
                }

                // we evaluated above, so now we need to do the rest of the algorithm, otherwise
                // Rust complains as we're in the wrong state of the state machine
                new_evaluated.select().recombine().mutate().reinsert().into()
            }
            Err(terminated) => {
                // a termination condition triggered here, so we may want to check the result
                break terminated;
            }
        };
    };

    let species = result.state().species();
    #[allow(clippy::expect_used)]
    let fittest = species
        .iter()
        .flat_map(|s| &s.population.members)
        .max_by(|a, b| a.fitness.total_cmp(&b.fitness))
        .expect("iterator can not be empty");

    println!(
        "generation: {}, fitness: {}, best member: \"{}\"",
        result.statistics().iteration,
        fittest.fitness,
        String::from_utf8_lossy(&fittest.phenotype.0),
    );
}