diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index faea71d..c999d27 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + workflow_dispatch: jobs: test: diff --git a/Cargo.lock b/Cargo.lock index b247f87..6dba5d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,26 +47,25 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "genetic-rs" -version = "0.6.0" +version = "1.0.0" dependencies = [ "genetic-rs-common", "genetic-rs-macros", - "rand 0.8.5", + "rand", ] [[package]] name = "genetic-rs-common" -version = "0.6.0" +version = "1.0.0" dependencies = [ - "rand 0.9.0", + "rand", "rayon", - "replace_with", "tracing", ] [[package]] name = "genetic-rs-macros" -version = "0.6.0" +version = "1.0.0" dependencies = [ "genetic-rs-common", "proc-macro2", @@ -74,17 +73,6 @@ dependencies = [ "syn", ] -[[package]] -name = "getrandom" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.3.2" @@ -94,7 +82,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi", ] [[package]] @@ -147,34 +135,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" -version = "0.8.5" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", - "zerocopy", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -184,16 +150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.12", + "rand_core", ] [[package]] @@ -202,7 +159,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom", ] [[package]] @@ -225,12 +182,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "replace_with" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" - [[package]] name = "syn" version = "2.0.51" @@ -279,12 +230,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -302,23 +247,3 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] diff --git a/Cargo.toml b/Cargo.toml index 4ca0520..75f3645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,9 @@ members = ["genetic-rs", "genetic-rs-common", "genetic-rs-macros"] resolver = "2" [workspace.package] -version = "0.6.0" +version = "1.0.0" authors = ["HyperCodec"] homepage = "https://github.com/hypercodec/genetic-rs" repository = "https://github.com/hypercodec/genetic-rs" license = "MIT" -edition = "2021" \ No newline at end of file +edition = "2021" diff --git a/README.md b/README.md index fad756c..e43a28c 100644 --- a/README.md +++ b/README.md @@ -6,74 +6,54 @@ A small crate for quickstarting genetic algorithm projects. -### How to Use -*note: if you are interested in implementing NEAT with this, try out the [neat](https://crates.io/crates/neat) crate* - ### Features First off, this crate comes with the `builtin`, `crossover`, and `genrand` features by default. If you want it to be parallelized, you can add the `rayon` feature. If you want your crossover to be speciated, you can add the `speciation` feature. -Once you have eveything imported as you wish, you can define your genome and impl the required traits: +### How to Use +> [!NOTE] +> If you are interested in implementing NEAT with this, try out the [neat](https://crates.io/crates/neat) crate + +Here's a simple genetic algorithm. ```rust -#[derive(Clone, Debug)] // clone is currently a required derive for pruning nextgens. +use genetic_rs::prelude::*; + +// `Mitosis` can be derived if both `Clone` and `RandomlyMutable` are present. +#[derive(Clone, Debug, Mitosis)] struct MyGenome { field1: f32, } -// required in all of the builtin functions as requirements of `DivsionReproduction` and `CrossoverReproduction` +// required in all of the builtin Repopulators as requirements of `Mitosis` and `Crossover` impl RandomlyMutable for MyGenome { fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { self.field1 += rng.gen::() * rate; } } -// required for `division_pruning_nextgen`. -impl DivsionReproduction for MyGenome { - fn divide(&self, rng: &mut impl ng) -> Self { - let mut child = self.clone(); - child.mutate(0.25, rng); // use a constant mutation rate when spawning children in pruning algorithms. - child - } -} - -// required for the builtin pruning algorithms. -impl Prunable for MyGenome { - fn despawn(self) { - // unneccessary to implement this function, but it can be useful for debugging and cleaning up genomes. - println!("{:?} died", self); - } -} - -// allows us to use `Vec::gen_random` for the initial population. +// allows us to use `Vec::gen_random` for the initial population. note that `Vec::gen_random` has a slightly different function signature depending on whether the `rayon` feature is enabled. impl GenerateRandom for MyGenome { - fn gen_random(rng: &mut impl rand::Rng) -> Self { + fn gen_random(rng: &mut impl Rng) -> Self { Self { field1: rng.gen() } } } -``` -Once you have a struct, you must create your fitness function: -```rust fn my_fitness_fn(ent: &MyGenome) -> f32 { // this just means that the algorithm will try to create as big a number as possible due to fitness being directly taken from the field. // in a more complex genetic algorithm, you will want to utilize `ent` to test them and generate a reward. ent.field1 } -``` - -Once you have your reward function, you can create a `GeneticSim` object to manage and control the evolutionary steps: - -```rust fn main() { let mut rng = rand::rng(); let mut sim = GeneticSim::new( // you must provide a random starting population. // size will be preserved in builtin nextgen fns, but it is not required to keep a constant size if you were to build your own nextgen function. // in this case, the compiler can infer the type of `Vec::gen_random` because of the input of `my_fitness_fn`. - Vec::gen_random(&mut rng, 100), - my_fitness_fn, - division_pruning_nextgen, + // this is the `rayon` feature signature. + Vec::gen_random(100), + FitnessEliminator::new_with_default(my_fitness_fn), + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) @@ -83,7 +63,7 @@ fn main() { } ``` -That is the minimal code for a working pruning-based genetic algorithm. You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/genetic-rs/examples/) for more complicated systems. +That is the minimal code for a working genetic algorithm on default features (+ rayon). You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/genetic-rs/examples/) for more complicated systems. I highly recommend looking into crossover reproduction, as it tends to produce better results than mitosis. ### License This project falls under the `MIT` license. diff --git a/genetic-rs-common/Cargo.toml b/genetic-rs-common/Cargo.toml index 83de467..f7f92d3 100644 --- a/genetic-rs-common/Cargo.toml +++ b/genetic-rs-common/Cargo.toml @@ -13,10 +13,10 @@ categories = ["algorithms", "science", "simulation"] [features] default = ["builtin", "genrand", "crossover"] -builtin = [] +builtin = ["dep:rand"] crossover = ["builtin"] speciation = ["crossover"] -genrand = [] +genrand = ["dep:rand"] rayon = ["dep:rayon"] tracing = ["dep:tracing"] @@ -27,7 +27,6 @@ features = ["crossover", "speciation"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -replace_with = "0.1.7" -rand = "0.9.0" +rand = { version = "0.9.2", optional = true } rayon = { version = "1.10.0", optional = true } -tracing = { version = "0.1.41", optional = true } \ No newline at end of file +tracing = { version = "0.1.41", optional = true } diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs new file mode 100644 index 0000000..10f9ac3 --- /dev/null +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -0,0 +1,122 @@ +use crate::Eliminator; +use crate::FeatureBoundedGenome; + +#[cfg(feature = "rayon")] +use rayon::prelude::*; + +/// A trait for fitness functions. This allows for more flexibility in defining fitness functions. +/// Any `Fn(&G) -> f32` can be used as a fitness function. +pub trait FitnessFn { + /// Evaluates a genome's fitness + fn fitness(&self, genome: &G) -> f32; +} + +impl FitnessFn for F +where + F: Fn(&G) -> f32, +{ + fn fitness(&self, genome: &G) -> f32 { + (self)(genome) + } +} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(not(feature = "rayon"))] +pub trait FeatureBoundedFitnessFn: FitnessFn {} +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedFitnessFn for T {} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(feature = "rayon")] +pub trait FeatureBoundedFitnessFn: FitnessFn + Send + Sync {} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedFitnessFn for T {} + +/// A fitness-based eliminator that eliminates genomes based on their fitness scores. +pub struct FitnessEliminator, G: FeatureBoundedGenome> { + /// The fitness function used to evaluate genomes. + pub fitness_fn: F, + + /// The percentage of genomes to keep. Must be between 0.0 and 1.0. + pub threshold: f32, + + _marker: std::marker::PhantomData, +} + +impl FitnessEliminator +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, +{ + /// Creates a new [`FitnessEliminator`] with a given fitness function and threshold. + /// Panics if the threshold is not between 0.0 and 1.0. + pub fn new(fitness_fn: F, threshold: f32) -> Self { + if !(0.0..=1.0).contains(&threshold) { + panic!("Threshold must be between 0.0 and 1.0"); + } + Self { + fitness_fn, + threshold, + _marker: std::marker::PhantomData, + } + } + + /// Creates a new [`FitnessEliminator`] with a default threshold of 0.5 (all genomes below median fitness are eliminated). + pub fn new_with_default(fitness_fn: F) -> Self { + Self::new(fitness_fn, 0.5) + } + + /// Calculates the fitness of each genome and sorts them by fitness. + /// Returns a vector of tuples containing the genome and its fitness score. + #[cfg(not(feature = "rayon"))] + pub fn calculate_and_sort(&self, genomes: Vec) -> Vec<(G, f32)> { + let mut fitnesses: Vec<(G, f32)> = genomes + .into_iter() + .map(|g| { + let fit = self.fitness_fn.fitness(&g); + (g, fit) + }) + .collect(); + fitnesses.sort_by(|(_a, afit), (_b, bfit)| bfit.partial_cmp(afit).unwrap()); + fitnesses + } + + /// Calculates the fitness of each genome and sorts them by fitness. + /// Returns a vector of tuples containing the genome and its fitness score. + #[cfg(feature = "rayon")] + pub fn calculate_and_sort(&self, genomes: Vec) -> Vec<(G, f32)> { + let mut fitnesses: Vec<(G, f32)> = genomes + .into_par_iter() + .map(|g| { + let fit = self.fitness_fn.fitness(&g); + (g, fit) + }) + .collect(); + fitnesses.sort_by(|(_a, afit), (_b, bfit)| bfit.partial_cmp(afit).unwrap()); + fitnesses + } +} + +impl Eliminator for FitnessEliminator +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, +{ + #[cfg(not(feature = "rayon"))] + fn eliminate(&self, genomes: Vec) -> Vec { + let mut fitnesses = self.calculate_and_sort(genomes); + let median_index = (fitnesses.len() as f32) * self.threshold; + fitnesses.truncate(median_index as usize + 1); + fitnesses.into_iter().map(|(g, _)| g).collect() + } + + #[cfg(feature = "rayon")] + fn eliminate(&self, genomes: Vec) -> Vec { + let mut fitnesses = self.calculate_and_sort(genomes); + let median_index = (fitnesses.len() as f32) * self.threshold; + fitnesses.truncate(median_index as usize + 1); + fitnesses.into_par_iter().map(|(g, _)| g).collect() + } +} diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs new file mode 100644 index 0000000..3e29be2 --- /dev/null +++ b/genetic-rs-common/src/builtin/mod.rs @@ -0,0 +1,5 @@ +/// Contains types implementing [`Eliminator`][crate::Eliminator] +pub mod eliminator; + +/// Contains types implementing [`Repopulator`][crate::Repopulator] +pub mod repopulator; diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs new file mode 100644 index 0000000..de0af12 --- /dev/null +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -0,0 +1,214 @@ +use rand::Rng as RandRng; + +use crate::{Repopulator, Rng}; + +#[cfg(feature = "tracing")] +use tracing::*; + +/// Used in other traits to randomly mutate genomes a given amount +pub trait RandomlyMutable { + /// Mutate the genome with a given mutation rate (0..1) + fn mutate(&mut self, rate: f32, rng: &mut impl Rng); +} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(not(feature = "tracing"))] +pub trait FeatureBoundedRandomlyMutable: RandomlyMutable {} +#[cfg(not(feature = "tracing"))] +impl FeatureBoundedRandomlyMutable for T {} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(feature = "tracing")] +pub trait FeatureBoundedRandomlyMutable: RandomlyMutable + std::fmt::Debug {} +#[cfg(feature = "tracing")] +impl FeatureBoundedRandomlyMutable for T {} + +/// Used in dividually-reproducing [`Repopulator`]s +pub trait Mitosis: Clone + FeatureBoundedRandomlyMutable { + /// Create a new child with mutation. Similar to [RandomlyMutable::mutate], but returns a new instance instead of modifying the original. + fn divide(&self, rate: f32, rng: &mut impl Rng) -> Self { + let mut child = self.clone(); + child.mutate(rate, rng); + child + } +} + +/// Used in crossover-reproducing [`Repopulator`]s +#[cfg(all(feature = "crossover", not(feature = "tracing")))] +#[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] +pub trait Crossover: Clone + PartialEq { + /// Use crossover reproduction to create a new genome. + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self; +} + +/// Used in crossover-reproducing [`next_gen`]s +#[cfg(all(feature = "crossover", feature = "tracing"))] +#[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] +pub trait Crossover: Clone + std::fmt::Debug { + /// Use crossover reproduction to create a new genome. + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self; +} + +/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. +#[cfg(feature = "speciation")] +#[cfg_attr(docsrs, doc(cfg(feature = "speciation")))] +pub trait Speciated: Sized { + /// Calculates whether two genomes are similar enough to be considered part of the same species. + fn is_same_species(&self, other: &Self) -> bool; + + /// Filters a list of genomes based on whether they are of the same species. + fn filter_same_species<'a>(&'a self, genomes: &'a [Self]) -> Vec<&'a Self> { + genomes.iter().filter(|g| self.is_same_species(g)).collect() + } +} + +/// Repopulator that uses division reproduction to create new genomes. +pub struct MitosisRepopulator { + /// The mutation rate to use when mutating genomes. 0.0 - 1.0 + pub mutation_rate: f32, + _marker: std::marker::PhantomData, +} + +impl MitosisRepopulator { + /// Creates a new [`MitosisRepopulator`]. + pub fn new(mutation_rate: f32) -> Self { + Self { + mutation_rate, + _marker: std::marker::PhantomData, + } + } +} + +impl Repopulator for MitosisRepopulator +where + G: Mitosis, +{ + fn repopulate(&self, genomes: &mut Vec, target_size: usize) { + let mut rng = rand::rng(); + let champions = genomes.clone(); + let mut champs_cycle = champions.iter().cycle(); + + // TODO maybe rayonify + while genomes.len() < target_size { + let parent = champs_cycle.next().unwrap(); + let child = parent.divide(self.mutation_rate, &mut rng); + genomes.push(child); + } + } +} + +/// Repopulator that uses crossover reproduction to create new genomes. +pub struct CrossoverRepopulator { + /// The mutation rate to use when mutating genomes. 0.0 - 1.0 + pub mutation_rate: f32, + _marker: std::marker::PhantomData, +} + +impl CrossoverRepopulator { + /// Creates a new [`CrossoverRepopulator`]. + pub fn new(mutation_rate: f32) -> Self { + Self { + mutation_rate, + _marker: std::marker::PhantomData, + } + } +} + +impl Repopulator for CrossoverRepopulator +where + G: Crossover, +{ + fn repopulate(&self, genomes: &mut Vec, target_size: usize) { + let mut rng = rand::rng(); + let champions = genomes.clone(); + let mut champs_cycle = champions.iter().enumerate().cycle(); + + // TODO maybe rayonify + while genomes.len() < target_size { + let (i, parent1) = champs_cycle.next().unwrap(); + let mut j = rng.random_range(1..champions.len()); + if i == j { + j = 0; + } + let parent2 = &genomes[j]; + + #[cfg(feature = "tracing")] + let span = span!( + Level::DEBUG, + "crossover", + a = tracing::field::debug(parent1), + b = tracing::field::debug(parent2) + ); + #[cfg(feature = "tracing")] + let enter = span.enter(); + + let child = parent1.crossover(parent2, self.mutation_rate, &mut rng); + + #[cfg(feature = "tracing")] + drop(enter); + + genomes.push(child); + } + } +} + +/// Repopulator that uses crossover reproduction to create new genomes, but only between genomes of the same species. +#[cfg(feature = "speciation")] +pub struct SpeciatedCrossoverRepopulator { + /// The mutation rate to use when mutating genomes. 0.0 - 1.0 + pub mutation_rate: f32, + _marker: std::marker::PhantomData, +} + +#[cfg(feature = "speciation")] +impl SpeciatedCrossoverRepopulator { + /// Creates a new [`SpeciatedCrossoverRepopulator`]. + pub fn new(mutation_rate: f32) -> Self { + Self { + mutation_rate, + _marker: std::marker::PhantomData, + } + } +} + +#[cfg(feature = "speciation")] +impl Repopulator for SpeciatedCrossoverRepopulator +where + G: Crossover + Speciated + PartialEq, +{ + fn repopulate(&self, genomes: &mut Vec, target_size: usize) { + let mut rng = rand::rng(); + let champions = genomes.clone(); + let mut champs_cycle = champions.iter().cycle(); + + // TODO maybe rayonify + while genomes.len() < target_size { + let parent1 = champs_cycle.next().unwrap(); + let mut parent2 = &champions[rng.random_range(0..champions.len() - 1)]; + + while parent1 == parent2 || !parent1.is_same_species(parent2) { + // TODO panic or eliminate if this parent cannot find another survivor in the same species + parent2 = &champions[rng.random_range(0..champions.len() - 1)]; + } + + #[cfg(feature = "tracing")] + let span = span!( + Level::DEBUG, + "crossover", + a = tracing::field::debug(parent1), + b = tracing::field::debug(parent2) + ); + #[cfg(feature = "tracing")] + let enter = span.enter(); + + let child = parent1.crossover(parent2, self.mutation_rate, &mut rng); + + #[cfg(feature = "tracing")] + drop(enter); + + genomes.push(child); + } + } +} diff --git a/genetic-rs-common/src/builtin.rs b/genetic-rs-common/src/builtin_old.rs similarity index 64% rename from genetic-rs-common/src/builtin.rs rename to genetic-rs-common/src/builtin_old.rs index eba1202..b641caa 100644 --- a/genetic-rs-common/src/builtin.rs +++ b/genetic-rs-common/src/builtin_old.rs @@ -1,93 +1,6 @@ use crate::Rng; -use rand::Rng as RandRNG; -/// Used in all of the builtin [`next_gen`]s to randomly mutate genomes a given amount -#[cfg(not(feature = "tracing"))] -pub trait RandomlyMutable { - /// Mutate the genome with a given mutation rate (0..1) - fn mutate(&mut self, rate: f32, rng: &mut impl Rng); -} - -/// Used in all of the builtin [`next_gen`]s to randomly mutate genomes a given amount -#[cfg(feature = "tracing")] -pub trait RandomlyMutable: std::fmt::Debug { - /// Mutate the genome with a given mutation rate (0..1) - fn mutate(&mut self, rate: f32, rng: &mut impl Rng); -} - -/// Used in dividually-reproducing [`next_gen`]s -#[cfg(not(feature = "tracing"))] -pub trait DivisionReproduction { - /// Create a new child with mutation. Similar to [RandomlyMutable::mutate], but returns a new instance instead of modifying the original. - /// If it is simply returning a cloned and mutated version, consider using a constant mutation rate. - fn divide(&self, rng: &mut impl Rng) -> Self; -} - -/// Used in dividually-reproducing [`next_gen`]s -#[cfg(feature = "tracing")] -pub trait DivisionReproduction: std::fmt::Debug { - /// Create a new child with mutation. Similar to [RandomlyMutable::mutate], but returns a new instance instead of modifying the original. - /// If it is simply returning a cloned and mutated version, consider using a constant mutation rate. - fn divide(&self, rng: &mut impl Rng) -> Self; -} - -/// Used in crossover-reproducing [`next_gen`]s -#[cfg(all(feature = "crossover", not(feature = "tracing")))] -#[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait CrossoverReproduction { - /// Use crossover reproduction to create a new genome. - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self; -} - -/// Used in crossover-reproducing [`next_gen`]s -#[cfg(all(feature = "crossover", feature = "tracing"))] -#[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait CrossoverReproduction: std::fmt::Debug { - /// Use crossover reproduction to create a new genome. - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self; -} - -#[cfg(not(feature = "tracing"))] -/// Used in pruning [`next_gen`]s -pub trait Prunable: Sized { - /// This does any unfinished work in the despawning process. - /// It doesn't need to be implemented unless in specific usecases where your algorithm needs to explicitly despawn a genome. - fn despawn(self) {} -} - -#[cfg(feature = "tracing")] -/// Used in pruning [`next_gen`]s -pub trait Prunable: Sized + std::fmt::Debug { - /// This does any unfinished work in the despawning process. - /// It doesn't need to be implemented unless in specific usecases where your algorithm needs to explicitly despawn a genome. - fn despawn(self) {} -} - -/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too dissimilar. -#[cfg(all(feature = "speciation", not(feature = "tracing")))] -#[cfg_attr(docsrs, doc(cfg(feature = "speciation")))] -pub trait Speciated: Sized { - /// Calculates whether two genomes are similar enough to be considered part of the same species. - fn is_same_species(&self, other: &Self) -> bool; - - /// Filters a list of genomes based on whether they are of the same species. - fn filter_same_species<'a>(&'a self, genomes: &'a [Self]) -> Vec<&Self> { - genomes.iter().filter(|g| self.is_same_species(g)).collect() - } -} - -/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too dissimilar. -#[cfg(all(feature = "speciation", feature = "tracing"))] -#[cfg_attr(docsrs, doc(cfg(feature = "speciation")))] -pub trait Speciated: Sized + std::fmt::Debug { - /// Calculates whether two genomes are similar enough to be considered part of the same species. - fn is_same_species(&self, other: &Self) -> bool; - - /// Filters a list of genomes based on whether they are of the same species. - fn filter_same_species<'a>(&self, genomes: &'a [Self]) -> Vec<&'a Self> { - genomes.iter().filter(|g| self.is_same_species(g)).collect() - } -} +// TODO clean up all this spaghetti and replace with eliminator and repopulator traits /// Provides some basic nextgens for [`GeneticSim`][crate::GeneticSim]. pub mod next_gen { @@ -98,93 +11,7 @@ pub mod next_gen { #[cfg(feature = "tracing")] use tracing::*; - - /// When making a new generation, it mutates each genome a certain amount depending on their reward. - /// This nextgen is very situational and should not be your first choice. - #[cfg_attr(feature = "tracing", instrument)] - pub fn scrambling_nextgen(mut rewards: Vec<(G, f32)>) -> Vec { - rewards.sort_by(|(_, r1), (_, r2)| r1.partial_cmp(r2).unwrap()); - - let len = rewards.len() as f32; - let mut rng = rand::rng(); - - rewards - .into_iter() - .enumerate() - .map(|(i, (mut g, _))| { - let rate = i as f32 / len; - - #[cfg(feature = "tracing")] - let span = span!( - Level::DEBUG, - "scramble_mutate", - index = i, - genome = tracing::field::debug(&g), - rate = rate - ); - #[cfg(feature = "tracing")] - let enter = span.enter(); - - g.mutate(rate, &mut rng); - - #[cfg(feature = "tracing")] - drop(enter); - - g - }) - .collect() - } - - /// When making a new generation, it despawns half of the genomes and then spawns children from the remaining to reproduce. - #[cfg(not(feature = "rayon"))] - #[cfg_attr(feature = "tracing", instrument)] - pub fn division_pruning_nextgen( - rewards: Vec<(G, f32)>, - ) -> Vec { - let population_size = rewards.len(); - let mut next_gen = pruning_helper(rewards); - - let mut rng = rand::rng(); - - let mut og_champions = next_gen - .clone() // TODO remove if possible. currently doing so because `next_gen` is borrowed as mutable later - .into_iter() - .cycle(); - - while next_gen.len() < population_size { - let g = og_champions.next().unwrap(); - - next_gen.push(g.divide(&mut rng)); - } - - next_gen - } - - /// Rayon version of the [`division_pruning_nextgen`] function - #[cfg(feature = "rayon")] - #[cfg_attr(feature = "tracing", instrument)] - pub fn division_pruning_nextgen( - rewards: Vec<(G, f32)>, - ) -> Vec { - let population_size = rewards.len(); - let mut next_gen = pruning_helper(rewards); - - let mut rng = rand::rng(); - - let mut og_champions = next_gen - .clone() // TODO remove if possible. currently doing so because `next_gen` is borrowed as mutable later - .into_iter() - .cycle(); - - while next_gen.len() < population_size { - let g = og_champions.next().unwrap(); - - next_gen.push(g.divide(&mut rng)); - } - - next_gen - } - + /// Prunes half of the genomes and randomly crosses over the remaining ones. #[cfg(all(feature = "crossover", not(feature = "rayon")))] #[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index 97aa63f..a3d2c8f 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -4,8 +4,6 @@ //! The crate containing the core traits and structs of genetic-rs. -use replace_with::replace_with_or_abort; - /// Built-in nextgen functions and traits to go with them. #[cfg_attr(docsrs, doc(cfg(feature = "builtin")))] #[cfg(feature = "builtin")] @@ -35,151 +33,102 @@ pub trait Rng: rand::Rng {} #[cfg(not(feature = "tracing"))] impl Rng for T {} -/// Represents a fitness function. Inputs a reference to the genome and outputs an f32. -pub trait FitnessFn { - /// Evaluates a genome's fitness - fn fitness(&self, genome: &G) -> f32; +/// Tests and eliminates the unfit from the simulation. +pub trait Eliminator { + /// Tests and eliminates the unfit from the simulation. + fn eliminate(&self, genomes: Vec) -> Vec; } -impl f32, G> FitnessFn for F { - fn fitness(&self, genome: &G) -> f32 { - (self)(genome) - } +/// Refills the population of the simulation based on survivors. +pub trait Repopulator { + /// Replaces the genomes in the simulation. + fn repopulate(&self, genomes: &mut Vec, target_size: usize); } -/// Represents a nextgen function. Inputs genomes and rewards and produces the next generation -pub trait NextgenFn { - /// Creates the next generation from the current fitness values. - fn next_gen(&self, fitness: Vec<(G, f32)>) -> Vec; -} +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(not(feature = "rayon"))] +pub trait FeatureBoundedEliminator: Eliminator {} +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedEliminator for T {} -impl) -> Vec, G> NextgenFn for F { - fn next_gen(&self, fitness: Vec<(G, f32)>) -> Vec { - (self)(fitness) - } -} +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(feature = "rayon")] +pub trait FeatureBoundedEliminator: Eliminator + Send + Sync {} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedEliminator for T {} -/// The simulation controller. -/// ```rust -/// use genetic_rs_common::prelude::*; -/// -/// #[derive(Debug, Clone)] -/// struct MyGenome { -/// a: f32, -/// b: f32, -/// } -/// -/// impl RandomlyMutable for MyGenome { -/// fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { -/// self.a += rng.random::() * rate; -/// self.b += rng.random::() * rate; -/// } -/// } -/// -/// impl DivisionReproduction for MyGenome { -/// fn divide(&self, rng: &mut impl Rng) -> Self { -/// let mut child = self.clone(); -/// child.mutate(0.25, rng); // you'll generally want to use a constant mutation rate for mutating children. -/// child -/// } -/// } -/// -/// impl Prunable for MyGenome {} // if we wanted to, we could implement the `despawn` function to run any cleanup code as needed. in this example, though, we do not need it. -/// -/// impl GenerateRandom for MyGenome { -/// fn gen_random(rng: &mut impl Rng) -> Self { -/// Self { -/// a: rng.gen(), -/// b: rng.gen(), -/// } -/// } -/// } -/// -/// fn main() { -/// let my_fitness_fn = |e: &MyGenome| { -/// e.a * e.b // should result in genomes increasing their value -/// }; -/// -/// let mut rng = rand::rng(); -/// -/// let mut sim = GeneticSim::new( -/// Vec::gen_random(&mut rng, 1000), -/// my_fitness_fn, -/// division_pruning_nextgen, -/// ); -/// -/// for _ in 0..100 { -/// // if this were a more complex simulation, you might test genomes in `sim.genomes` between `next_generation` calls to provide a more accurate reward. -/// sim.next_generation(); -/// } -/// -/// dbg!(sim.genomes); -/// } -/// ``` +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. #[cfg(not(feature = "rayon"))] -pub struct GeneticSim -where - F: FitnessFn, - NG: NextgenFn, - G: Sized, -{ - /// The current population of genomes - pub genomes: Vec, - fitness: F, - next_gen: NG, -} +pub trait FeatureBoundedRepopulator: Repopulator {} +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedRepopulator for T {} -/// Rayon version of the [`GeneticSim`] struct +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. #[cfg(feature = "rayon")] -pub struct GeneticSim -where - F: FitnessFn + Send + Sync, - NG: NextgenFn + Send + Sync, - G: Sized + Send, -{ +pub trait FeatureBoundedRepopulator: Repopulator + Send + Sync {} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedRepopulator for T {} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(not(feature = "rayon"))] +pub trait FeatureBoundedGenome {} +#[cfg(not(feature = "rayon"))] +impl FeatureBoundedGenome for T {} + +/// Internal trait that simply deals with the trait bounds of features to avoid duplicate code. +/// It is blanket implemented, so you should never have to reference this directly. +#[cfg(feature = "rayon")] +pub trait FeatureBoundedGenome: Sized + Send + Sync {} +#[cfg(feature = "rayon")] +impl FeatureBoundedGenome for T {} + +/// This struct is the main entry point for the simulation. It handles the state and evolution of the genomes +/// based on what eliminator and repopulator it receives. +pub struct GeneticSim, R: FeatureBoundedRepopulator> { /// The current population of genomes pub genomes: Vec, - fitness: F, - next_gen: NG, + + /// The eliminator used to eliminate unfit genomes + pub eliminator: E, + + /// The repopulator used to refill the population + pub repopulator: R, } -#[cfg(not(feature = "rayon"))] -impl GeneticSim +impl GeneticSim where - F: FitnessFn, - NG: NextgenFn, G: Sized, + E: FeatureBoundedEliminator, + R: FeatureBoundedRepopulator, { /// Creates a [`GeneticSim`] with a given population of `starting_genomes` (the size of which will be retained), /// a given fitness function, and a given nextgen function. - pub fn new(starting_genomes: Vec, fitness: F, next_gen: NG) -> Self { + pub fn new(starting_genomes: Vec, eliminator: E, repopulator: R) -> Self { Self { genomes: starting_genomes, - fitness, - next_gen, + eliminator, + repopulator, } } - /// Uses the `next_gen` provided in [`GeneticSim::new`] to create the next generation of genomes. + /// Uses the [`Eliminator`] and [`Repopulator`] provided in [`GeneticSim::new`] to create the next generation of genomes. pub fn next_generation(&mut self) { - // TODO maybe remove unneccessary dependency, can prob use std::mem::replace #[cfg(feature = "tracing")] let span = span!(Level::TRACE, "next_generation"); #[cfg(feature = "tracing")] let enter = span.enter(); - replace_with_or_abort(&mut self.genomes, |genomes| { - let rewards = genomes - .into_iter() - .map(|g| { - let fitness: f32 = self.fitness.fitness(&g); - (g, fitness) - }) - .collect(); + let genomes = std::mem::take(&mut self.genomes); - self.next_gen.next_gen(rewards) - }); + let target_size = genomes.len(); + self.genomes = self.eliminator.eliminate(genomes); + self.repopulator.repopulate(&mut self.genomes, target_size); #[cfg(feature = "tracing")] drop(enter); @@ -193,46 +142,6 @@ where } } -#[cfg(feature = "rayon")] -impl GeneticSim -where - F: FitnessFn + Send + Sync, - NG: NextgenFn + Send + Sync, - G: Sized + Send, -{ - /// Creates a [`GeneticSim`] with a given population of `starting_genomes` (the size of which will be retained), - /// a given fitness function, and a given nextgen function. - pub fn new(starting_genomes: Vec, fitness: F, next_gen: NG) -> Self { - Self { - genomes: starting_genomes, - fitness, - next_gen, - } - } - - /// Performs selection and produces the next generation within the simulation. - pub fn next_generation(&mut self) { - replace_with_or_abort(&mut self.genomes, |genomes| { - let rewards = genomes - .into_par_iter() - .map(|e| { - let fitness: f32 = self.fitness.fitness(&e); - (e, fitness) - }) - .collect(); - - self.next_gen.next_gen(rewards) - }); - } - - /// Calls [`next_generation`][GeneticSim::next_generation] `count` number of times. - pub fn perform_generations(&mut self, count: usize) { - for _ in 0..count { - self.next_generation(); - } - } -} - /// Helper trait used in the generation of random starting populations #[cfg(feature = "genrand")] #[cfg_attr(docsrs, doc(cfg(feature = "genrand")))] @@ -288,19 +197,3 @@ where .collect() } } - -#[cfg(test)] -mod tests { - use super::prelude::*; - - #[test] - fn send_sim() { - let mut sim = GeneticSim::new(vec![()], |_: &()| 0., |_: Vec<((), f32)>| vec![()]); - - let h = std::thread::spawn(move || { - sim.next_generation(); - }); - - h.join().unwrap(); - } -} diff --git a/genetic-rs-common/src/prelude.rs b/genetic-rs-common/src/prelude.rs index d487bad..d8d60b5 100644 --- a/genetic-rs-common/src/prelude.rs +++ b/genetic-rs-common/src/prelude.rs @@ -3,9 +3,6 @@ pub extern crate rand; pub use crate::*; #[cfg(feature = "builtin")] -pub use crate::builtin::*; - -#[cfg(feature = "builtin")] -pub use next_gen::*; +pub use crate::builtin::{eliminator::*, repopulator::*}; pub use rand::Rng as RandRng; diff --git a/genetic-rs-macros/Cargo.toml b/genetic-rs-macros/Cargo.toml index 360d0e0..53891a6 100644 --- a/genetic-rs-macros/Cargo.toml +++ b/genetic-rs-macros/Cargo.toml @@ -22,7 +22,7 @@ genrand = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -genetic-rs-common = { path = "../genetic-rs-common", version = "0.6.0" } +genetic-rs-common = { path = "../genetic-rs-common", version = "1.0.0" } proc-macro2 = "1.0.78" quote = "1.0.35" syn = "2.0.51" diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 3f1f8e4..acd3050 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -2,138 +2,123 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Fields}; +use quote::quote_spanned; +use syn::spanned::Spanned; +use syn::{parse_macro_input, Data, DeriveInput}; #[proc_macro_derive(RandomlyMutable)] pub fn randmut_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let mut inner_mutate = quote!(); + let name = ast.ident; - if let Data::Struct(data) = ast.data { - match &data.fields { - Fields::Named(named) => { - for field in named.named.iter() { - let name = field.ident.clone().unwrap(); - inner_mutate - .extend(quote!(RandomlyMutable::mutate(&mut self.#name, rate, rng);)); - } - } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive RandomlyMutable for an enum."); - } + match ast.data { + Data::Struct(s) => { + let mut inner = Vec::new(); - let name = &ast.ident; - quote! { - impl RandomlyMutable for #name { - fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { - #inner_mutate - } - } - } - .into() -} - -#[proc_macro_derive(DivisionReproduction)] -pub fn divrepr_derive(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); + for (i, field) in s.fields.into_iter().enumerate() { + let ty = field.ty; + let span = ty.span(); - let mut inner_divide_return = quote!(); - - if let Data::Struct(data) = ast.data { - match &data.fields { - Fields::Named(named) => { - for field in named.named.iter() { - let name = field.ident.clone().unwrap(); - inner_divide_return - .extend(quote!(#name: DivisionReproduction::divide(&self.#name, rng),)); + if let Some(field_name) = field.ident { + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#field_name, rate, rng); + }); + } else { + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::RandomlyMutable>::mutate(&mut self.#i, rate, rng); + }); } } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive DivisionReproduction for an enum."); - } - let name = &ast.ident; + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); - quote! { - impl DivisionReproduction for #name { - fn divide(&self, rng: &mut impl Rng) -> Self { - Self { - #inner_divide_return + quote! { + impl genetic_rs_common::prelude::RandomlyMutable for #name { + fn mutate(&mut self, rate: f32, rng: &mut impl genetic_rs_common::Rng) { + #inner + } } } + .into() + } + Data::Enum(_e) => { + panic!("enums not yet supported"); + } + Data::Union(_u) => { + panic!("unions not yet supported"); } } - .into() } -#[cfg(feature = "crossover")] -#[proc_macro_derive(CrossoverReproduction)] -pub fn cross_repr_derive(input: TokenStream) -> TokenStream { +#[proc_macro_derive(Mitosis)] +pub fn mitosis_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - - let mut inner_crossover_return = quote!(); - - if let Data::Struct(data) = ast.data { - match &data.fields { - Fields::Named(named) => { - for field in named.named.iter() { - let name = field.ident.clone().unwrap(); - inner_crossover_return.extend(quote!(#name: CrossoverReproduction::crossover(&self.#name, &other.#name, rng),)); - } - } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive CrossoverReproduction for an enum."); - } - let name = &ast.ident; quote! { - impl CrossoverReproduction for #name { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { - Self { #inner_crossover_return } - } - } + impl genetic_rs_common::prelude::Mitosis for #name {} } .into() } -#[proc_macro_derive(Prunable)] -pub fn prunable_derive(input: TokenStream) -> TokenStream { +#[cfg(feature = "crossover")] +#[proc_macro_derive(Crossover)] +pub fn crossover_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let mut inner_despawn = quote!(); - - if let Data::Struct(data) = ast.data { - match &data.fields { - Fields::Named(named) => { - for field in named.named.iter() { - let name = field.ident.clone().unwrap(); - inner_despawn.extend(quote!(Prunable::despawn(self.#name);)); + let name = ast.ident; + + match ast.data { + Data::Struct(s) => { + let mut inner = Vec::new(); + let mut tuple_struct = false; + + for (i, field) in s.fields.into_iter().enumerate() { + let ty = field.ty; + let span = ty.span(); + + if let Some(field_name) = field.ident { + inner.push(quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#field_name, &other.#field_name, rate, rng), + }); + } else { + tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#i, &other.#i, rate, rng), + }); } } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive Prunable for an enum."); - } - let name = &ast.ident; - - quote! { - impl Prunable for #name { - fn despawn(self) { - #inner_despawn + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + + if tuple_struct { + quote! { + impl genetic_rs_common::prelude::Crossover for #name { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { + Self(#inner) + } + } + }.into() + } else { + quote! { + impl genetic_rs_common::prelude::Crossover for #name { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { + Self { + #inner + } + } + } + }.into() } } + Data::Enum(_e) => { + panic!("enums not yet supported"); + } + Data::Union(_u) => { + panic!("unions not yet supported"); + } } - .into() } #[cfg(feature = "genrand")] @@ -141,31 +126,57 @@ pub fn prunable_derive(input: TokenStream) -> TokenStream { pub fn genrand_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let mut genrand_inner_return = quote!(); - - if let Data::Struct(data) = ast.data { - match &data.fields { - Fields::Named(named) => { - for field in named.named.iter() { - let name = field.ident.clone().unwrap(); - let ty = field.ty.clone(); - genrand_inner_return - .extend(quote!(#name: <#ty as GenerateRandom>::gen_random(rng),)); + let name = ast.ident; + + match ast.data { + Data::Struct(s) => { + let mut inner = Vec::new(); + let mut tuple_struct = false; + + for field in s.fields { + let ty = field.ty; + let span = ty.span(); + + if let Some(field_name) = field.ident { + inner.push(quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), + }); + } else { + tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), + }); } } - _ => unimplemented!(), - } - } - let name = &ast.ident; - quote! { - impl GenerateRandom for #name { - fn gen_random(rng: &mut impl Rng) -> Self { - Self { - #genrand_inner_return + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + if tuple_struct { + quote! { + impl genetic_rs_common::prelude::GenerateRandom for #name { + fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { + Self(#inner) + } + } + } + .into() + } else { + quote! { + impl genetic_rs_common::prelude::GenerateRandom for #name { + fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { + Self { + #inner + } + } + } } + .into() } } + Data::Enum(_e) => { + panic!("enums not yet supported"); + } + Data::Union(_u) => { + panic!("unions not yet supported"); + } } - .into() } diff --git a/genetic-rs/Cargo.toml b/genetic-rs/Cargo.toml index 4902da2..89e8454 100644 --- a/genetic-rs/Cargo.toml +++ b/genetic-rs/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["genetic", "algorithm", "rust"] categories = ["algorithms", "science", "simulation"] [features] -default = ["builtin", "genrand"] +default = ["builtin", "genrand", "crossover"] builtin = ["genetic-rs-common/builtin"] crossover = ["builtin", "genetic-rs-common/crossover", "genetic-rs-macros/crossover"] speciation = ["crossover", "genetic-rs-common/speciation"] @@ -22,11 +22,11 @@ derive = ["dep:genetic-rs-macros", "genetic-rs-common/builtin"] tracing = ["genetic-rs-common/tracing"] [dependencies] -genetic-rs-common = { path = "../genetic-rs-common", version = "0.6.0" } -genetic-rs-macros = { path = "../genetic-rs-macros", version = "0.6.0", optional = true } +genetic-rs-common = { path = "../genetic-rs-common", version = "1.0.0" } +genetic-rs-macros = { path = "../genetic-rs-macros", version = "1.0.0", optional = true } [dev-dependencies] -rand = "0.8.5" +rand = "0.9.2" [[example]] name = "crossover" diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index a68492c..872ae39 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -11,18 +11,16 @@ impl RandomlyMutable for MyGenome { } } -impl CrossoverReproduction for MyGenome { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { +impl Crossover for MyGenome { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self { let mut child = Self { val: (self.val + other.val) / 2., }; - child.mutate(0.25, rng); + child.mutate(rate, rng); child } } -impl Prunable for MyGenome {} - impl GenerateRandom for MyGenome { fn gen_random(rng: &mut impl Rng) -> Self { Self { @@ -35,13 +33,13 @@ impl GenerateRandom for MyGenome { fn main() { let mut rng = rand::rng(); - let magic_number = rng.gen::() * 1000.; - let fitness = move |e: &MyGenome| -> f32 { -(magic_number - e.val).abs() }; + let magic_number = rng.random::() * 1000.; + let fitness = move |e: &MyGenome| -> f32 { 1.0 / (magic_number - e.val).abs().max(1e-7) }; let mut sim = GeneticSim::new( Vec::gen_random(&mut rng, 100), - fitness, - crossover_pruning_nextgen, + FitnessEliminator::new_with_default(fitness), + CrossoverRepopulator::new(0.25), // 25% mutation rate ); sim.perform_generations(100); @@ -55,7 +53,11 @@ fn main() { let magic_number = rng.random::() * 1000.; let fitness = move |e: &MyGenome| -> f32 { -(magic_number - e.val).abs() }; - let mut sim = GeneticSim::new(Vec::gen_random(100), fitness, crossover_pruning_nextgen); + let mut sim = GeneticSim::new( + Vec::gen_random(100), + FitnessEliminator::new_with_default(fitness), + CrossoverRepopulator::new(0.25), // 25% mutation rate + ); for _ in 0..100 { sim.next_generation(); diff --git a/genetic-rs/examples/derive.rs b/genetic-rs/examples/derive.rs index 6f1d160..808360b 100644 --- a/genetic-rs/examples/derive.rs +++ b/genetic-rs/examples/derive.rs @@ -11,19 +11,11 @@ impl RandomlyMutable for TestGene { } } -impl DivisionReproduction for TestGene { - fn divide(&self, rng: &mut impl Rng) -> Self { - let mut child = self.clone(); - child.mutate(0.25, rng); - child - } -} - #[cfg(feature = "crossover")] -impl CrossoverReproduction for TestGene { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { +impl Crossover for TestGene { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self { Self { - a: (self.a + other.a + rng.random_range(-0.5..0.5)) / 2., + a: (self.a + other.a + rng.random_range(-1.0..1.0) * rate) / 2., } } } @@ -37,8 +29,8 @@ impl GenerateRandom for TestGene { } // using the derive macros here is only useful if the fields are not related to each other -#[derive(RandomlyMutable, DivisionReproduction, GenerateRandom, Clone, Debug)] -#[cfg_attr(feature = "crossover", derive(CrossoverReproduction))] +#[derive(RandomlyMutable, Mitosis, GenerateRandom, Clone, Debug)] +#[cfg_attr(feature = "crossover", derive(Crossover))] struct MyDNA { g1: TestGene, g2: TestGene, diff --git a/genetic-rs/examples/readme_ex.rs b/genetic-rs/examples/readme_ex.rs index c7b3740..8da08b2 100644 --- a/genetic-rs/examples/readme_ex.rs +++ b/genetic-rs/examples/readme_ex.rs @@ -7,29 +7,15 @@ struct MyGenome { field1: f32, } -// required in all of the builtin functions as requirements of `DivisionReproduction` and `CrossoverReproduction`. +// required in all of the builtin repopulators as requirements of `Mitosis` and `Crossover`. impl RandomlyMutable for MyGenome { fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { self.field1 += rng.random::() * rate; } } -// required for `division_pruning_nextgen`. -impl DivisionReproduction for MyGenome { - fn divide(&self, rng: &mut impl Rng) -> Self { - let mut child = self.clone(); - child.mutate(0.25, rng); // use a constant mutation rate when spawning children in pruning algorithms. - child - } -} - -// required for the builtin pruning algorithms. -impl Prunable for MyGenome { - fn despawn(self) { - // unneccessary to implement this function, but it can be useful for debugging and cleaning up genomes. - println!("{:?} died", self); - } -} +// use auto derives for the builtin nextgen functions to work with your genome. +impl Mitosis for MyGenome {} // helper trait that allows us to use `Vec::gen_random` for the initial population. impl GenerateRandom for MyGenome { @@ -54,8 +40,8 @@ fn main() { // size will be preserved in builtin nextgen fns, but it is not required to keep a constant size if you were to build your own nextgen function. // in this case, you do not need to specify a type for `Vec::gen_random` because of the input of `my_fitness_fn`. Vec::gen_random(&mut rng, 100), - my_fitness_fn, - division_pruning_nextgen, + FitnessEliminator::new_with_default(my_fitness_fn), + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) @@ -66,19 +52,19 @@ fn main() { #[cfg(feature = "rayon")] fn main() { + // the only difference between this and the non-rayon version is that we don't pass in rng. + let mut sim = GeneticSim::new( // you must provide a random starting population. // size will be preserved in builtin nextgen fns, but it is not required to keep a constant size if you were to build your own nextgen function. // in this case, you do not need to specify a type for `Vec::gen_random` because of the input of `my_fitness_fn`. Vec::gen_random(100), - my_fitness_fn, - division_pruning_nextgen, + FitnessEliminator::new_with_default(my_fitness_fn), + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) - for _ in 0..100 { - sim.next_generation(); // in a genetic algorithm with state, such as a physics simulation, you'd want to do things with `sim.randomomes` in between these calls - } + sim.perform_generations(100); dbg!(sim.genomes); } diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index 1e9e22c..c4cbf5e 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -13,21 +13,15 @@ impl RandomlyMutable for MyGenome { } } -impl DivisionReproduction for MyGenome { - fn divide(&self, rng: &mut impl Rng) -> Self { - let mut child = self.clone(); - child.mutate(0.25, rng); - child - } -} +impl Mitosis for MyGenome {} -impl CrossoverReproduction for MyGenome { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { +impl Crossover for MyGenome { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self { let mut child = Self { val1: (self.val1 + other.val1) / 2., val2: (self.val2 + other.val2) / 2., }; - child.mutate(0.25, rng); + child.mutate(rate, rng); child } } @@ -39,8 +33,6 @@ impl Speciated for MyGenome { } } -impl Prunable for MyGenome {} - impl GenerateRandom for MyGenome { fn gen_random(rng: &mut impl rand::Rng) -> Self { Self { @@ -61,11 +53,10 @@ fn main() { let mut sim = GeneticSim::new( Vec::gen_random(&mut rng, 100), - fitness, - speciated_crossover_pruning_nextgen, + FitnessEliminator::new_with_default(fitness), + SpeciatedCrossoverRepopulator::new(0.25), // 25% mutation rate ); - // speciation tends to take more generations (not needed to this extent, but the crate is fast enough to where it isn't much of a compromise) sim.perform_generations(100); dbg!(sim.genomes); @@ -75,13 +66,11 @@ fn main() { fn main() { let mut sim = GeneticSim::new( Vec::gen_random(100), - fitness, - speciated_crossover_pruning_nextgen, + FitnessEliminator::new_with_default(fitness), + SpeciatedCrossoverRepopulator::new(0.25), // 25% mutation rate ); - for _ in 0..1000 { - sim.next_generation(); - } + sim.perform_generations(100); dbg!(sim.genomes); } diff --git a/genetic-rs/src/lib.rs b/genetic-rs/src/lib.rs index f04bcea..95b5b06 100644 --- a/genetic-rs/src/lib.rs +++ b/genetic-rs/src/lib.rs @@ -1,92 +1,5 @@ #![allow(clippy::needless_doctest_main)] - -//! A small crate to quickstart genetic algorithm projects -//! -//! ### How to Use -//! -//! ### Features -//! First off, this crate comes with the `builtin` and `genrand` features by default. -//! If you want to add the builtin crossover reproduction extension, you can do so by adding the `crossover` feature. -//! If you want it to be parallelized, you can add the `rayon` feature. -//! If you want your crossover to be speciated, you can add the `speciation` feature. -//! -//! Once you have eveything imported as you wish, you can define your genomes and impl the required traits: -//! -//! ```rust, ignore -//! #[derive(Clone, Debug)] // clone is currently a required derive for pruning nextgens. -//! struct MyGenome { -//! field1: f32, -//! } -//! -//! // required in all of the builtin functions as requirements of `DivisionReproduction` and `CrossoverReproduction` -//! impl RandomlyMutable for MyGenome { -//! fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) { -//! self.field1 += rng.gen::() * rate; -//! } -//! } -//! -//! // required for `division_pruning_nextgen`. -//! impl DivisionReproduction for MyGenome { -//! fn divide(&self, rng: &mut impl rand::Rng) -> Self { -//! let mut child = self.clone(); -//! child.mutate(0.25, rng); // use a constant mutation rate when spawning children in pruning algorithms. -//! child -//! } -//! } -//! -//! // required for the builtin pruning algorithms. -//! impl Prunable for MyGenome { -//! fn despawn(self) { -//! // unneccessary to implement this function, but it can be useful for debugging and cleaning up genomes. -//! println!("{:?} died", self); -//! } -//! } -//! -//! // helper trait that allows us to use `Vec::gen_random` for the initial population. -//! impl GenerateRandom for MyGenome { -//! fn gen_random(rng: &mut impl rand::Rng) -> Self { -//! Self { field1: rng.gen() } -//! } -//! } -//! ``` -//! -//! Once you have a struct, you must create your fitness function: -//! ```rust, ignore -//! fn my_fitness_fn(ent: &MyGenome) -> f32 { -//! // this just means that the algorithm will try to create as big a number as possible due to fitness being directly taken from the field. -//! // in a more complex genetic algorithm, you will want to utilize `ent` to test them and generate a reward. -//! ent.field1 -//! } -//! ``` -//! -//! -//! Once you have your fitness function, you can create a [`GeneticSim`] object to manage and control the evolutionary steps: -//! -//! ```rust, ignore -//! fn main() { -//! let mut rng = rand::thread_rng(); -//! let mut sim = GeneticSim::new( -//! // you must provide a random starting population. -//! // size will be preserved in builtin nextgen fns, but it is not required to keep a constant size if you were to build your own nextgen function. -//! // in this case, you do not need to specify a type for `Vec::gen_random` because of the input of `my_fitness_fn`. -//! Vec::gen_random(&mut rng, 100), -//! my_fitness_fn, -//! division -//! ); -//! -//! // perform evolution (100 gens) -//! for _ in 0..100 { -//! sim.next_generation(); // in a genetic algorithm with state, such as a physics simulation, you'd want to do things with `sim.genomes` in between these calls -//! } -//! -//! dbg!(sim.genomes); -//! } -//! ``` -//! -//! That is the minimal code for a working pruning-based genetic algorithm. You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/examples/) for more complicated systems. -//! -//! ### License -//! This project falls under the `MIT` license. +#![doc = include_str!("../../README.md")] pub mod prelude { pub use genetic_rs_common::prelude::*;