From f86fad43a14ae3e49f3a49d178e6d6c80d9af502 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 17:19:25 -0400 Subject: [PATCH 01/28] use full path qualification in macros --- genetic-rs-macros/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 3f1f8e4..1221da7 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -16,7 +16,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { for field in named.named.iter() { let name = field.ident.clone().unwrap(); inner_mutate - .extend(quote!(RandomlyMutable::mutate(&mut self.#name, rate, rng);)); + .extend(quote!(genetic_rs_common::prelude::RandomlyMutable::mutate(&mut self.#name, rate, rng);)); } } _ => unimplemented!(), @@ -27,8 +27,8 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { let name = &ast.ident; quote! { - impl RandomlyMutable for #name { - fn mutate(&mut self, rate: f32, rng: &mut impl Rng) { + impl genetic_rs_common::prelude::RandomlyMutable for #name { + fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) { #inner_mutate } } @@ -48,7 +48,7 @@ pub fn divrepr_derive(input: TokenStream) -> TokenStream { for field in named.named.iter() { let name = field.ident.clone().unwrap(); inner_divide_return - .extend(quote!(#name: DivisionReproduction::divide(&self.#name, rng),)); + .extend(quote!(#name: genetic_rs_common::prelude::DivisionReproduction::divide(&self.#name, rng),)); } } _ => unimplemented!(), @@ -61,7 +61,7 @@ pub fn divrepr_derive(input: TokenStream) -> TokenStream { quote! { impl DivisionReproduction for #name { - fn divide(&self, rng: &mut impl Rng) -> Self { + fn divide(&self, rng: &mut impl rand::Rng) -> Self { Self { #inner_divide_return } @@ -83,7 +83,7 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { 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),)); + inner_crossover_return.extend(quote!(#name: genetic_rs_common::prelude::CrossoverReproduction::crossover(&self.#name, &other.#name, rng),)); } } _ => unimplemented!(), @@ -95,8 +95,8 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { let name = &ast.ident; quote! { - impl CrossoverReproduction for #name { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + impl genetic_rs_common::prelude::CrossoverReproduction for #name { + fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { Self { #inner_crossover_return } } } @@ -115,7 +115,7 @@ pub fn prunable_derive(input: TokenStream) -> TokenStream { Fields::Named(named) => { for field in named.named.iter() { let name = field.ident.clone().unwrap(); - inner_despawn.extend(quote!(Prunable::despawn(self.#name);)); + inner_despawn.extend(quote!(genetic_rs_common::prelude::Prunable::despawn(self.#name);)); } } _ => unimplemented!(), @@ -150,7 +150,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { let name = field.ident.clone().unwrap(); let ty = field.ty.clone(); genrand_inner_return - .extend(quote!(#name: <#ty as GenerateRandom>::gen_random(rng),)); + .extend(quote!(#name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng),)); } } _ => unimplemented!(), From caa47199ee90047f6d55bed258be7bc15d0e175a Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 18:01:20 -0400 Subject: [PATCH 02/28] implement basic eliminator/repopulator types --- genetic-rs-common/src/builtin/eliminators.rs | 69 +++++++ genetic-rs-common/src/builtin/mod.rs | 2 + genetic-rs-common/src/builtin/repopulator.rs | 147 +++++++++++++++ .../src/{builtin.rs => builtin_old.rs} | 177 +----------------- genetic-rs-common/src/lib.rs | 39 ++-- genetic-rs-common/src/prelude.rs | 2 +- genetic-rs-macros/src/lib.rs | 8 +- 7 files changed, 237 insertions(+), 207 deletions(-) create mode 100644 genetic-rs-common/src/builtin/eliminators.rs create mode 100644 genetic-rs-common/src/builtin/mod.rs create mode 100644 genetic-rs-common/src/builtin/repopulator.rs rename genetic-rs-common/src/{builtin.rs => builtin_old.rs} (64%) diff --git a/genetic-rs-common/src/builtin/eliminators.rs b/genetic-rs-common/src/builtin/eliminators.rs new file mode 100644 index 0000000..01a894a --- /dev/null +++ b/genetic-rs-common/src/builtin/eliminators.rs @@ -0,0 +1,69 @@ +use crate::Eliminator; + +#[cfg(feature = "rayon")] +use rayon::prelude::*; + +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) + } +} + +pub struct FitnessEliminator, G> { + 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, G> FitnessEliminator { + /// 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 threshold < 0.0 || threshold > 1.0 { + 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) + } +} + +impl, G> Eliminator for FitnessEliminator { + #[cfg(not(feature = "rayon"))] + fn eliminate(&self, genomes: Vec) -> Vec { + let mut fitnesses: Vec<(G, f32)> = genomes.iter().map(|g| (g, self.fitness_fn.fitness(&g))).collect(); + fitnesses.sort_by(|(_a, afit), (_b, bfit)| afit.partial_cmp(bfit).unwrap()); + 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: Vec<(G, f32)> = genomes.into_par_iter().map(|g| (g, self.fitness_fn.fitness(&g))).collect(); + fitnesses.sort_by(|(_a, afit), (_b, bfit)| afit.partial_cmp(bfit).unwrap()); + let median_index = (fitnesses.len() as f32) * self.threshold; + fitnesses.truncate(median_index as usize + 1); + fitnesses.into_par_iter().map(|(g, _)| g).collect() + } +} + +// TODO `ObservedFitnessEliminator` that sends the `fitnesses` to observer(s) before truncating \ No newline at end of file diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs new file mode 100644 index 0000000..aed9077 --- /dev/null +++ b/genetic-rs-common/src/builtin/mod.rs @@ -0,0 +1,2 @@ +pub mod eliminators; +pub mod repopulator; \ No newline at end of file diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs new file mode 100644 index 0000000..789b54d --- /dev/null +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -0,0 +1,147 @@ +use crate::{Repopulator, Rng}; + +/// 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: Clone { + /// 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, rate: f32, 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, rate: f32, 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, 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 CrossoverReproduction: 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 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() + } +} + +pub struct DivisionRepopulator { + /// The mutation rate to use when mutating genomes. 0.0 - 1.0 + pub mutation_rate: f32, + _marker: std::marker::PhantomData, +} + +impl DivisionRepopulator { + /// Creates a new [`DivisionRepopulator`]. + pub fn new(mutation_rate: f32) -> Self { + Self { + mutation_rate, + _marker: std::marker::PhantomData, + } + } +} + +impl Repopulator for DivisionRepopulator +where + G: DivisionReproduction +{ + fn repopulate(&self, genomes: &mut Vec, target_size: usize) { + let mut rng = rand::rng(); + let mut champions = genomes.clone().iter().cycle(); + + // TODO maybe rayonify + while genomes.len() < target_size { + let parent = champions.next().unwrap(); + let child = parent.divide(self.mutation_rate, &mut rng); + genomes.push(child); + } + } +} + +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: CrossoverReproduction, +{ + 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 = champions.next().unwrap(); + let parent2 = &champions[rng.random_range(0..champions.len() - 1)]; + + if parent1 == parent2 { + continue; + } + + let child = parent1.crossover(parent2, &mut rng); + genomes.push(child); + } + } +} \ No newline at end of file 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..af588a2 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -35,28 +35,14 @@ 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; +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) - } -} - -/// 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; -} - -impl) -> Vec, G> NextgenFn for F { - fn next_gen(&self, fitness: Vec<(G, f32)>) -> Vec { - (self)(fitness) - } +pub trait Repopulator { + /// Replaces the genomes in the simulation. + fn repopulate(&self, genomes: &mut Vec, target_size: usize); } /// The simulation controller. @@ -117,16 +103,16 @@ impl) -> Vec, G> NextgenFn for F { /// } /// ``` #[cfg(not(feature = "rayon"))] -pub struct GeneticSim +pub struct GeneticSim where - F: FitnessFn, - NG: NextgenFn, G: Sized, + E: Eliminator, + R: Repopulator, { /// The current population of genomes pub genomes: Vec, - fitness: F, - next_gen: NG, + pub eliminator: E, + pub repopulator: R, } /// Rayon version of the [`GeneticSim`] struct @@ -144,10 +130,9 @@ where } #[cfg(not(feature = "rayon"))] -impl GeneticSim +impl GeneticSim where F: FitnessFn, - NG: NextgenFn, G: Sized, { /// Creates a [`GeneticSim`] with a given population of `starting_genomes` (the size of which will be retained), diff --git a/genetic-rs-common/src/prelude.rs b/genetic-rs-common/src/prelude.rs index d487bad..ba64e77 100644 --- a/genetic-rs-common/src/prelude.rs +++ b/genetic-rs-common/src/prelude.rs @@ -3,7 +3,7 @@ pub extern crate rand; pub use crate::*; #[cfg(feature = "builtin")] -pub use crate::builtin::*; +pub use crate::builtin_old::*; #[cfg(feature = "builtin")] pub use next_gen::*; diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 1221da7..2f09c8a 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -28,7 +28,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { let name = &ast.ident; quote! { impl genetic_rs_common::prelude::RandomlyMutable for #name { - fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) { + fn mutate(&mut self, rate: f32, rng: &mut impl genetic_rs_common::Rng) { #inner_mutate } } @@ -61,7 +61,7 @@ pub fn divrepr_derive(input: TokenStream) -> TokenStream { quote! { impl DivisionReproduction for #name { - fn divide(&self, rng: &mut impl rand::Rng) -> Self { + fn divide(&self, rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #inner_divide_return } @@ -96,7 +96,7 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { quote! { impl genetic_rs_common::prelude::CrossoverReproduction for #name { - fn crossover(&self, other: &Self, rng: &mut impl rand::Rng) -> Self { + fn crossover(&self, other: &Self, rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #inner_crossover_return } } } @@ -160,7 +160,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { let name = &ast.ident; quote! { impl GenerateRandom for #name { - fn gen_random(rng: &mut impl Rng) -> Self { + fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #genrand_inner_return } From c7ab90b50a963ee6a439bef0018d3b1ea7e3c527 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 18:58:09 -0400 Subject: [PATCH 03/28] implement speciation (needs improvement), fix some bugs, and test the api with readme_ex --- genetic-rs-common/Cargo.toml | 6 +- .../builtin/{eliminators.rs => eliminator.rs} | 44 +++++- genetic-rs-common/src/builtin/mod.rs | 2 +- genetic-rs-common/src/builtin/repopulator.rs | 125 +++++++++++++++--- genetic-rs-common/src/lib.rs | 101 ++++++-------- genetic-rs-common/src/prelude.rs | 5 +- genetic-rs/examples/readme_ex.rs | 34 ++--- 7 files changed, 202 insertions(+), 115 deletions(-) rename genetic-rs-common/src/builtin/{eliminators.rs => eliminator.rs} (53%) diff --git a/genetic-rs-common/Cargo.toml b/genetic-rs-common/Cargo.toml index 83de467..b8bac89 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"] @@ -28,6 +28,6 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] replace_with = "0.1.7" -rand = "0.9.0" +rand = { version = "0.9.0", optional = true } rayon = { version = "1.10.0", optional = true } tracing = { version = "0.1.41", optional = true } \ No newline at end of file diff --git a/genetic-rs-common/src/builtin/eliminators.rs b/genetic-rs-common/src/builtin/eliminator.rs similarity index 53% rename from genetic-rs-common/src/builtin/eliminators.rs rename to genetic-rs-common/src/builtin/eliminator.rs index 01a894a..2f643f9 100644 --- a/genetic-rs-common/src/builtin/eliminators.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -3,6 +3,8 @@ use crate::Eliminator; #[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; @@ -17,7 +19,9 @@ where } } +/// A fitness-based eliminator that eliminates genomes based on their fitness scores. pub struct FitnessEliminator, G> { + /// 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. @@ -44,13 +48,42 @@ impl, G> FitnessEliminator { 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, G> Eliminator for FitnessEliminator { #[cfg(not(feature = "rayon"))] fn eliminate(&self, genomes: Vec) -> Vec { - let mut fitnesses: Vec<(G, f32)> = genomes.iter().map(|g| (g, self.fitness_fn.fitness(&g))).collect(); - fitnesses.sort_by(|(_a, afit), (_b, bfit)| afit.partial_cmp(bfit).unwrap()); + 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() @@ -58,12 +91,9 @@ impl, G> Eliminator for FitnessEliminator { #[cfg(feature = "rayon")] fn eliminate(&self, genomes: Vec) -> Vec { - let mut fitnesses: Vec<(G, f32)> = genomes.into_par_iter().map(|g| (g, self.fitness_fn.fitness(&g))).collect(); - fitnesses.sort_by(|(_a, afit), (_b, bfit)| afit.partial_cmp(bfit).unwrap()); + 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() } -} - -// TODO `ObservedFitnessEliminator` that sends the `fitnesses` to observer(s) before truncating \ No newline at end of file +} \ No newline at end of file diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs index aed9077..2df4f48 100644 --- a/genetic-rs-common/src/builtin/mod.rs +++ b/genetic-rs-common/src/builtin/mod.rs @@ -1,2 +1,2 @@ -pub mod eliminators; +pub mod eliminator; pub mod repopulator; \ No newline at end of file diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 789b54d..34f006a 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -1,3 +1,5 @@ +use rand::Rng as RandRng; + use crate::{Repopulator, Rng}; /// Used in all of the builtin [`next_gen`]s to randomly mutate genomes a given amount @@ -17,24 +19,32 @@ pub trait RandomlyMutable: std::fmt::Debug { /// Used in dividually-reproducing [`next_gen`]s #[cfg(not(feature = "tracing"))] -pub trait DivisionReproduction: Clone { +pub trait DivisionReproduction: Clone + RandomlyMutable { /// 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, rate: f32, rng: &mut impl Rng) -> Self; + fn divide(&self, rate: f32, rng: &mut impl Rng) -> Self { + let mut child = self.clone(); + child.mutate(rate, rng); + child + } } /// Used in dividually-reproducing [`next_gen`]s #[cfg(feature = "tracing")] -pub trait DivisionReproduction: std::fmt::Debug { +pub trait DivisionReproduction: Clone + RandomlyMutable + 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, rate: f32, rng: &mut impl Rng) -> Self; + fn divide(&self, rate: f32, rng: &mut impl Rng) -> Self { + let mut child = self.clone(); + child.mutate(rate, rng); + child + } } /// Used in crossover-reproducing [`next_gen`]s #[cfg(all(feature = "crossover", not(feature = "tracing")))] #[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait CrossoverReproduction { +pub trait CrossoverReproduction: Clone { /// Use crossover reproduction to create a new genome. fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self; } @@ -42,12 +52,12 @@ pub trait CrossoverReproduction { /// 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 { +pub trait CrossoverReproduction: 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 dissimilar. +/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. #[cfg(all(feature = "speciation", not(feature = "tracing")))] #[cfg_attr(docsrs, doc(cfg(feature = "speciation")))] pub trait Speciated: Sized { @@ -55,12 +65,12 @@ pub trait Speciated: Sized { 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> { + fn filter_same_species<'a>(&'a self, genomes: &'a [Self]) -> Vec<&'a 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. +/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. #[cfg(all(feature = "speciation", feature = "tracing"))] #[cfg_attr(docsrs, doc(cfg(feature = "speciation")))] pub trait Speciated: Sized + std::fmt::Debug { @@ -73,6 +83,7 @@ pub trait Speciated: Sized + std::fmt::Debug { } } +/// Repopulator that uses division reproduction to create new genomes. pub struct DivisionRepopulator { /// The mutation rate to use when mutating genomes. 0.0 - 1.0 pub mutation_rate: f32, @@ -95,24 +106,26 @@ where { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); - let mut champions = genomes.clone().iter().cycle(); + let champions = genomes.clone(); + let mut champs_cycle = champions.iter().cycle(); // TODO maybe rayonify while genomes.len() < target_size { - let parent = champions.next().unwrap(); + let parent = champs_cycle.next().unwrap(); let child = parent.divide(self.mutation_rate, &mut rng); genomes.push(child); } } } -pub struct CrossoverRepopulator { +/// 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 { +impl CrossoverRepopulator { /// Creates a new [`CrossoverRepopulator`]. pub fn new(mutation_rate: f32) -> Self { Self { @@ -124,7 +137,7 @@ impl CrossoverRepopulator { impl Repopulator for CrossoverRepopulator where - G: CrossoverReproduction, + G: CrossoverReproduction + PartialEq, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); @@ -133,14 +146,88 @@ where // TODO maybe rayonify while genomes.len() < target_size { - let parent1 = champions.next().unwrap(); - let parent2 = &champions[rng.random_range(0..champions.len() - 1)]; + let parent1 = champs_cycle.next().unwrap(); + let mut parent2 = &champions[rng.random_range(0..champions.len() - 1)]; - if parent1 == parent2 { - continue; + while parent1 == parent2 { + // TODO bad way to do this, but it works for now + parent2 = &champions[rng.random_range(0..champions.len() - 1)]; } - let child = parent1.crossover(parent2, &mut rng); + #[cfg(feature = "tracing")] + let span = span!( + Level::DEBUG, + "crossover", + a = tracing::field::debug(g1), + b = tracing::field::debug(g2) + ); + #[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: CrossoverReproduction + 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(g1), + b = tracing::field::debug(g2) + ); + #[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/lib.rs b/genetic-rs-common/src/lib.rs index af588a2..f3b94e9 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -4,6 +4,7 @@ //! The crate containing the core traits and structs of genetic-rs. +use builtin::repopulator; use replace_with::replace_with_or_abort; /// Built-in nextgen functions and traits to go with them. @@ -103,11 +104,11 @@ pub trait Repopulator { /// } /// ``` #[cfg(not(feature = "rayon"))] -pub struct GeneticSim -where +pub struct GeneticSim< G: Sized, E: Eliminator, R: Repopulator, +> { /// The current population of genomes pub genomes: Vec, @@ -117,35 +118,36 @@ where /// Rayon version of the [`GeneticSim`] struct #[cfg(feature = "rayon")] -pub struct GeneticSim -where - F: FitnessFn + Send + Sync, - NG: NextgenFn + Send + Sync, - G: Sized + Send, +pub struct GeneticSim< + G: Sized + Sync, + E: Eliminator + Send + Sync, + R: Repopulator + Send + Sync, +> { /// The current population of genomes pub genomes: Vec, - fitness: F, - next_gen: NG, + pub eliminator: E, + pub repopulator: R, } #[cfg(not(feature = "rayon"))] -impl GeneticSim +impl GeneticSim where - F: FitnessFn, G: Sized, + E: Eliminator, + R: Repopulator, { /// 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")] @@ -155,15 +157,10 @@ where 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(); - - self.next_gen.next_gen(rewards) + let target_size = genomes.len(); + let mut new_genomes = self.eliminator.eliminate(genomes); + self.repopulator.repopulate(&mut new_genomes, target_size); + new_genomes }); #[cfg(feature = "tracing")] @@ -179,35 +176,41 @@ where } #[cfg(feature = "rayon")] -impl GeneticSim +impl GeneticSim where - F: FitnessFn + Send + Sync, - NG: NextgenFn + Send + Sync, G: Sized + Send, + E: Eliminator + Send + Sync, + R: Repopulator + Send + Sync, { /// 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, } } - /// Performs selection and produces the next generation within the simulation. + + /// 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_par_iter() - .map(|e| { - let fitness: f32 = self.fitness.fitness(&e); - (e, fitness) - }) - .collect(); - - self.next_gen.next_gen(rewards) + let target_size = genomes.len(); + let mut new_genomes = self.eliminator.eliminate(genomes); + self.repopulator.repopulate(&mut new_genomes, target_size); + new_genomes }); + + #[cfg(feature = "tracing")] + drop(enter); } /// Calls [`next_generation`][GeneticSim::next_generation] `count` number of times. @@ -272,20 +275,4 @@ where .map(|_| T::gen_random(&mut rand::rng())) .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(); - } -} +} \ No newline at end of file diff --git a/genetic-rs-common/src/prelude.rs b/genetic-rs-common/src/prelude.rs index ba64e77..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_old::*; - -#[cfg(feature = "builtin")] -pub use next_gen::*; +pub use crate::builtin::{eliminator::*, repopulator::*}; pub use rand::Rng as RandRng; diff --git a/genetic-rs/examples/readme_ex.rs b/genetic-rs/examples/readme_ex.rs index c7b3740..eaa5d70 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 `DivisionReproduction` and `CrossoverReproduction`. 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 DivisionReproduction 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), + DivisionRepopulator::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), + DivisionRepopulator::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); } From f7b2edab8a0b062887e396466757774033ac6e8c Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:02:31 -0400 Subject: [PATCH 04/28] mess with some of the lib.rs docs --- genetic-rs-common/src/builtin/mod.rs | 3 ++ genetic-rs-common/src/lib.rs | 66 ++++------------------------ 2 files changed, 11 insertions(+), 58 deletions(-) diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs index 2df4f48..e26e6c7 100644 --- a/genetic-rs-common/src/builtin/mod.rs +++ b/genetic-rs-common/src/builtin/mod.rs @@ -1,2 +1,5 @@ +/// Contains types implementing [`Eliminator`] pub mod eliminator; + +/// Contains types implementing [`Repopulator`] pub mod repopulator; \ No newline at end of file diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index f3b94e9..1980022 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -4,7 +4,6 @@ //! The crate containing the core traits and structs of genetic-rs. -use builtin::repopulator; use replace_with::replace_with_or_abort; /// Built-in nextgen functions and traits to go with them. @@ -36,73 +35,20 @@ pub trait Rng: rand::Rng {} #[cfg(not(feature = "tracing"))] impl Rng for T {} +/// 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; } +/// 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); } -/// 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); -/// } -/// ``` +/// 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. #[cfg(not(feature = "rayon"))] pub struct GeneticSim< G: Sized, @@ -112,7 +58,11 @@ pub struct GeneticSim< { /// The current population of genomes pub genomes: Vec, + + /// The eliminator used to eliminate unfit genomes pub eliminator: E, + + /// The repopulator used to refill the population pub repopulator: R, } From 01b0066ff7725aad939e98670921f47a3efdd61c Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:04:28 -0400 Subject: [PATCH 05/28] remove Prunable macro and simplify division macro --- genetic-rs-macros/src/lib.rs | 58 +------------------------------ genetic-rs/examples/speciation.rs | 14 ++------ 2 files changed, 4 insertions(+), 68 deletions(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 2f09c8a..5789275 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -39,34 +39,10 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { #[proc_macro_derive(DivisionReproduction)] pub fn divrepr_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - - 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: genetic_rs_common::prelude::DivisionReproduction::divide(&self.#name, rng),)); - } - } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive DivisionReproduction for an enum."); - } - let name = &ast.ident; quote! { - impl DivisionReproduction for #name { - fn divide(&self, rng: &mut impl genetic_rs_common::Rng) -> Self { - Self { - #inner_divide_return - } - } - } + impl genetic_rs_common::prelude::DivisionReproduction for #name {} } .into() } @@ -104,38 +80,6 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { .into() } -#[proc_macro_derive(Prunable)] -pub fn prunable_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!(genetic_rs_common::prelude::Prunable::despawn(self.#name);)); - } - } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive Prunable for an enum."); - } - - let name = &ast.ident; - - quote! { - impl Prunable for #name { - fn despawn(self) { - #inner_despawn - } - } - } - .into() -} - #[cfg(feature = "genrand")] #[proc_macro_derive(GenerateRandom)] pub fn genrand_derive(input: TokenStream) -> TokenStream { diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index 1e9e22c..b982ea3 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 DivisionReproduction for MyGenome {} impl CrossoverReproduction for MyGenome { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + 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 { From 65f4571ea64334fa863507c998d8937324f09b4f Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:07:03 -0400 Subject: [PATCH 06/28] fix examples --- genetic-rs/examples/crossover.rs | 12 +++++------- genetic-rs/examples/derive.rs | 8 -------- genetic-rs/examples/speciation.rs | 4 ++-- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index a68492c..3886b8a 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -12,17 +12,15 @@ impl RandomlyMutable for MyGenome { } impl CrossoverReproduction for MyGenome { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + 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 magic_number = rng.random::() * 1000.; let fitness = move |e: &MyGenome| -> f32 { -(magic_number - e.val).abs() }; 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); diff --git a/genetic-rs/examples/derive.rs b/genetic-rs/examples/derive.rs index 6f1d160..d4d66e4 100644 --- a/genetic-rs/examples/derive.rs +++ b/genetic-rs/examples/derive.rs @@ -11,14 +11,6 @@ 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 { diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index b982ea3..dce01c2 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -53,8 +53,8 @@ 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) From 51615f037740b669da34f035c32f2c15b3fb3ad0 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:13:08 -0400 Subject: [PATCH 07/28] satisfy clippy --- genetic-rs-common/src/builtin/eliminator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs index 2f643f9..364216d 100644 --- a/genetic-rs-common/src/builtin/eliminator.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -34,7 +34,7 @@ impl, G> FitnessEliminator { /// 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 threshold < 0.0 || threshold > 1.0 { + if !(0.0..=1.0).contains(&threshold) { panic!("Threshold must be between 0.0 and 1.0"); } Self { From b29dac7a98edae9eaf4f08e533762850a21c3a0e Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:22:31 -0400 Subject: [PATCH 08/28] fix rayon feature --- genetic-rs-common/src/builtin/eliminator.rs | 15 ++++++++++++--- genetic-rs-common/src/lib.rs | 6 +++++- genetic-rs/examples/crossover.rs | 6 +++++- genetic-rs/examples/speciation.rs | 9 +++------ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs index 364216d..1e53317 100644 --- a/genetic-rs-common/src/builtin/eliminator.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -20,7 +20,7 @@ where } /// A fitness-based eliminator that eliminates genomes based on their fitness scores. -pub struct FitnessEliminator, G> { +pub struct FitnessEliminator, G: Sized + Send> { /// The fitness function used to evaluate genomes. pub fitness_fn: F, @@ -30,7 +30,12 @@ pub struct FitnessEliminator, G> { _marker: std::marker::PhantomData, } -impl, G> FitnessEliminator { +// TODO only require `Send` and `Sync` for `F` when `rayon` is enabled` +impl FitnessEliminator +where + F: FitnessFn + Send + Sync, + G: Sized + Send + Sync +{ /// 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 { @@ -80,7 +85,11 @@ impl, G> FitnessEliminator { } } -impl, G> Eliminator for FitnessEliminator { +impl Eliminator for FitnessEliminator +where + F: FitnessFn + Send + Sync, + G: Sized + Send + Sync +{ #[cfg(not(feature = "rayon"))] fn eliminate(&self, genomes: Vec) -> Vec { let mut fitnesses = self.calculate_and_sort(genomes); diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index 1980022..a93c778 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -76,7 +76,11 @@ pub struct GeneticSim< { /// The current population of genomes pub genomes: Vec, + + /// The eliminator used to eliminate unfit genomes pub eliminator: E, + + /// The repopulator used to refill the population pub repopulator: R, } @@ -128,7 +132,7 @@ where #[cfg(feature = "rayon")] impl GeneticSim where - G: Sized + Send, + G: Sized + Send + Sync, E: Eliminator + Send + Sync, R: Repopulator + Send + Sync, { diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index 3886b8a..3bb5bb9 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -53,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/speciation.rs b/genetic-rs/examples/speciation.rs index dce01c2..8fd126f 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -57,7 +57,6 @@ fn main() { 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); @@ -67,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); } From 6ecfcec70edb5f6eb30a188f4a47aeaa6eb0f281 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:23:00 -0400 Subject: [PATCH 09/28] cargo fmt --- genetic-rs-common/src/builtin/eliminator.rs | 8 ++++---- genetic-rs-common/src/builtin/mod.rs | 2 +- genetic-rs-common/src/builtin/repopulator.rs | 5 ++--- genetic-rs-common/src/lib.rs | 13 +++---------- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs index 1e53317..32f8558 100644 --- a/genetic-rs-common/src/builtin/eliminator.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -34,7 +34,7 @@ pub struct FitnessEliminator, G: Sized + Send> { impl FitnessEliminator where F: FitnessFn + Send + Sync, - G: Sized + Send + Sync + G: Sized + Send + Sync, { /// Creates a new [`FitnessEliminator`] with a given fitness function and threshold. /// Panics if the threshold is not between 0.0 and 1.0. @@ -45,7 +45,7 @@ where Self { fitness_fn, threshold, - _marker: std::marker::PhantomData + _marker: std::marker::PhantomData, } } @@ -88,7 +88,7 @@ where impl Eliminator for FitnessEliminator where F: FitnessFn + Send + Sync, - G: Sized + Send + Sync + G: Sized + Send + Sync, { #[cfg(not(feature = "rayon"))] fn eliminate(&self, genomes: Vec) -> Vec { @@ -105,4 +105,4 @@ where fitnesses.truncate(median_index as usize + 1); fitnesses.into_par_iter().map(|(g, _)| g).collect() } -} \ No newline at end of file +} diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs index e26e6c7..12a5060 100644 --- a/genetic-rs-common/src/builtin/mod.rs +++ b/genetic-rs-common/src/builtin/mod.rs @@ -2,4 +2,4 @@ pub mod eliminator; /// Contains types implementing [`Repopulator`] -pub mod repopulator; \ No newline at end of file +pub mod repopulator; diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 34f006a..21e05ba 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -16,7 +16,6 @@ pub trait RandomlyMutable: std::fmt::Debug { fn mutate(&mut self, rate: f32, rng: &mut impl Rng); } - /// Used in dividually-reproducing [`next_gen`]s #[cfg(not(feature = "tracing"))] pub trait DivisionReproduction: Clone + RandomlyMutable { @@ -102,7 +101,7 @@ impl DivisionRepopulator { impl Repopulator for DivisionRepopulator where - G: DivisionReproduction + G: DivisionReproduction, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); @@ -231,4 +230,4 @@ where genomes.push(child); } } -} \ No newline at end of file +} diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index a93c778..65be52b 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -50,12 +50,7 @@ pub trait Repopulator { /// 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. #[cfg(not(feature = "rayon"))] -pub struct GeneticSim< - G: Sized, - E: Eliminator, - R: Repopulator, -> -{ +pub struct GeneticSim, R: Repopulator> { /// The current population of genomes pub genomes: Vec, @@ -72,8 +67,7 @@ pub struct GeneticSim< G: Sized + Sync, E: Eliminator + Send + Sync, R: Repopulator + Send + Sync, -> -{ +> { /// The current population of genomes pub genomes: Vec, @@ -146,7 +140,6 @@ where } } - /// 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 @@ -229,4 +222,4 @@ where .map(|_| T::gen_random(&mut rand::rng())) .collect() } -} \ No newline at end of file +} From 6739309d51ca22f2adf5f689f0b2b871a032ca01 Mon Sep 17 00:00:00 2001 From: HyperCodec Date: Sat, 3 May 2025 19:24:49 -0400 Subject: [PATCH 10/28] fix tracing feature --- genetic-rs-common/src/builtin/repopulator.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 21e05ba..0dd7da8 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -2,6 +2,9 @@ use rand::Rng as RandRng; use crate::{Repopulator, Rng}; +#[cfg(feature = "tracing")] +use tracing::*; + /// Used in all of the builtin [`next_gen`]s to randomly mutate genomes a given amount #[cfg(not(feature = "tracing"))] pub trait RandomlyMutable { @@ -157,8 +160,8 @@ where let span = span!( Level::DEBUG, "crossover", - a = tracing::field::debug(g1), - b = tracing::field::debug(g2) + a = tracing::field::debug(parent1), + b = tracing::field::debug(parent2) ); #[cfg(feature = "tracing")] let enter = span.enter(); @@ -216,8 +219,8 @@ where let span = span!( Level::DEBUG, "crossover", - a = tracing::field::debug(g1), - b = tracing::field::debug(g2) + a = tracing::field::debug(parent1), + b = tracing::field::debug(parent2) ); #[cfg(feature = "tracing")] let enter = span.enter(); From 2aebd6ed480d2333ecaa2bb0bfe5f39e3fdda4e4 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:03:18 +0000 Subject: [PATCH 11/28] add workflow dispatch to CI/CD --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) 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: From 97c65106039a24ca3975e6e08b06b770626fceea Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:27:28 +0000 Subject: [PATCH 12/28] rewrite genrand macro --- Cargo.lock | 7 ---- Cargo.toml | 2 +- genetic-rs-common/Cargo.toml | 3 +- genetic-rs-common/src/lib.rs | 14 +++---- genetic-rs-macros/src/lib.rs | 72 ++++++++++++++++++++++++------------ 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b247f87..c3a201e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,6 @@ version = "0.6.0" dependencies = [ "rand 0.9.0", "rayon", - "replace_with", "tracing", ] @@ -225,12 +224,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" diff --git a/Cargo.toml b/Cargo.toml index 4ca0520..484c588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ 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/genetic-rs-common/Cargo.toml b/genetic-rs-common/Cargo.toml index b8bac89..a41f7e4 100644 --- a/genetic-rs-common/Cargo.toml +++ b/genetic-rs-common/Cargo.toml @@ -27,7 +27,6 @@ features = ["crossover", "speciation"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -replace_with = "0.1.7" rand = { version = "0.9.0", 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/lib.rs b/genetic-rs-common/src/lib.rs index 65be52b..8ef0e03 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")] @@ -97,19 +95,17 @@ where /// 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 target_size = genomes.len(); - let mut new_genomes = self.eliminator.eliminate(genomes); - self.repopulator.repopulate(&mut new_genomes, target_size); - new_genomes - }); + let genomes = std::mem::take(&mut self.genomes); + + let target_size = genomes.len(); + self.genomes = self.eliminator.eliminate(genomes); + self.repopulator.repopulate(&mut self.genomes, target_size); #[cfg(feature = "tracing")] drop(enter); diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 5789275..58a4e37 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -3,6 +3,8 @@ 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; #[proc_macro_derive(RandomlyMutable)] pub fn randmut_derive(input: TokenStream) -> TokenStream { @@ -59,7 +61,7 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { Fields::Named(named) => { for field in named.named.iter() { let name = field.ident.clone().unwrap(); - inner_crossover_return.extend(quote!(#name: genetic_rs_common::prelude::CrossoverReproduction::crossover(&self.#name, &other.#name, rng),)); + inner_crossover_return.extend(quote!(#name: genetic_rs_common::prelude::CrossoverReproduction::crossover(&self.#name, &other.#name, rate, rng),)); } } _ => unimplemented!(), @@ -72,7 +74,7 @@ pub fn cross_repr_derive(input: TokenStream) -> TokenStream { quote! { impl genetic_rs_common::prelude::CrossoverReproduction for #name { - fn crossover(&self, other: &Self, rng: &mut impl genetic_rs_common::Rng) -> Self { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #inner_crossover_return } } } @@ -85,31 +87,55 @@ pub fn cross_repr_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 genetic_rs_common::prelude::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 GenerateRandom>::gen_random(rng), + }); + } else { + tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as GenerateRandom>::gen_random(rng), + }); } } - _ => unimplemented!(), - } - } - let name = &ast.ident; - quote! { - impl GenerateRandom for #name { - fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { - Self { - #genrand_inner_return - } + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + if tuple_struct { + quote! { + impl GenerateRandom for #name { + fn gen_random(rng: &mut impl rand::Rng) -> Self { + Self(#inner) + } + } + }.into() + } else { + quote! { + impl GenerateRandom for #name { + fn gen_random(rng: &mut impl rand::Rng) -> Self { + Self { + #inner + } + } + } + }.into() } + }, + Data::Enum(_e) => { + panic!("enums not yet supported"); + }, + Data::Union(_u) => { + panic!("unions not yet supported"); } } - .into() } From b54d8209d5fc6da3c8c4f2daf096d8c2a0a4a897 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:40:25 +0000 Subject: [PATCH 13/28] rename some traits and rewrite crossover macro --- genetic-rs-common/src/builtin/repopulator.rs | 31 +++++---- genetic-rs-macros/src/lib.rs | 70 ++++++++++++++------ genetic-rs/examples/crossover.rs | 2 +- genetic-rs/examples/derive.rs | 2 +- genetic-rs/examples/speciation.rs | 2 +- 5 files changed, 66 insertions(+), 41 deletions(-) diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 0dd7da8..2757ac9 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -21,9 +21,8 @@ pub trait RandomlyMutable: std::fmt::Debug { /// Used in dividually-reproducing [`next_gen`]s #[cfg(not(feature = "tracing"))] -pub trait DivisionReproduction: Clone + RandomlyMutable { +pub trait Mitosis: Clone + RandomlyMutable { /// 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, rate: f32, rng: &mut impl Rng) -> Self { let mut child = self.clone(); child.mutate(rate, rng); @@ -33,7 +32,7 @@ pub trait DivisionReproduction: Clone + RandomlyMutable { /// Used in dividually-reproducing [`next_gen`]s #[cfg(feature = "tracing")] -pub trait DivisionReproduction: Clone + RandomlyMutable + std::fmt::Debug { +pub trait Mitosis: Clone + RandomlyMutable + 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, rate: f32, rng: &mut impl Rng) -> Self { @@ -46,7 +45,7 @@ pub trait DivisionReproduction: Clone + RandomlyMutable + std::fmt::Debug { /// Used in crossover-reproducing [`next_gen`]s #[cfg(all(feature = "crossover", not(feature = "tracing")))] #[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait CrossoverReproduction: Clone { +pub trait Crossover: Clone { /// Use crossover reproduction to create a new genome. fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self; } @@ -54,7 +53,7 @@ pub trait CrossoverReproduction: Clone { /// Used in crossover-reproducing [`next_gen`]s #[cfg(all(feature = "crossover", feature = "tracing"))] #[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait CrossoverReproduction: Clone + std::fmt::Debug { +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; } @@ -86,14 +85,14 @@ pub trait Speciated: Sized + std::fmt::Debug { } /// Repopulator that uses division reproduction to create new genomes. -pub struct DivisionRepopulator { +pub struct MitosisRepopulator { /// The mutation rate to use when mutating genomes. 0.0 - 1.0 pub mutation_rate: f32, _marker: std::marker::PhantomData, } -impl DivisionRepopulator { - /// Creates a new [`DivisionRepopulator`]. +impl MitosisRepopulator { + /// Creates a new [`MitosisRepopulator`]. pub fn new(mutation_rate: f32) -> Self { Self { mutation_rate, @@ -102,9 +101,9 @@ impl DivisionRepopulator { } } -impl Repopulator for DivisionRepopulator +impl Repopulator for MitosisRepopulator where - G: DivisionReproduction, + G: Mitosis, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); @@ -121,13 +120,13 @@ where } /// Repopulator that uses crossover reproduction to create new genomes. -pub struct CrossoverRepopulator { +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 { +impl CrossoverRepopulator { /// Creates a new [`CrossoverRepopulator`]. pub fn new(mutation_rate: f32) -> Self { Self { @@ -139,7 +138,7 @@ impl CrossoverRepopulator { impl Repopulator for CrossoverRepopulator where - G: CrossoverReproduction + PartialEq, + G: Crossover + PartialEq, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); @@ -178,14 +177,14 @@ where /// Repopulator that uses crossover reproduction to create new genomes, but only between genomes of the same species. #[cfg(feature = "speciation")] -pub struct SpeciatedCrossoverRepopulator { +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 { +impl SpeciatedCrossoverRepopulator { /// Creates a new [`SpeciatedCrossoverRepopulator`]. pub fn new(mutation_rate: f32) -> Self { Self { @@ -198,7 +197,7 @@ impl SpeciatedCrossoverRepopul #[cfg(feature = "speciation")] impl Repopulator for SpeciatedCrossoverRepopulator where - G: CrossoverReproduction + Speciated + PartialEq, + G: Crossover + Speciated + PartialEq, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 58a4e37..7c56860 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -39,7 +39,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { } #[proc_macro_derive(DivisionReproduction)] -pub fn divrepr_derive(input: TokenStream) -> TokenStream { +pub fn mitosis_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; @@ -50,36 +50,62 @@ pub fn divrepr_derive(input: TokenStream) -> TokenStream { } #[cfg(feature = "crossover")] -#[proc_macro_derive(CrossoverReproduction)] -pub fn cross_repr_derive(input: TokenStream) -> TokenStream { +#[proc_macro_derive(Crossover)] +pub fn crossover_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let mut inner_crossover_return = 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_crossover_return.extend(quote!(#name: genetic_rs_common::prelude::CrossoverReproduction::crossover(&self.#name, &other.#name, rate, rng),)); + match ast.data { + Data::Struct(s) => { + let mut inner = Vec::new(); + let mut tuple_struct = false; + + for (i, field) in s.fields.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 Crossover>::crossover(&self.#field_name, &other.#field_name, rate, rng), + }); + } else { + tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as Crossover>::crossover(&self.#i, &other.#i, rate, rng), + }); } } - _ => unimplemented!(), - } - } else { - panic!("Cannot derive CrossoverReproduction for an enum."); - } - let name = &ast.ident; + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); - quote! { - impl genetic_rs_common::prelude::CrossoverReproduction for #name { - fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { - Self { #inner_crossover_return } + if tuple_struct { + quote! { + impl Crossover for #name { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::Rng) -> Self { + Self(#inner) + } + } + }.into() + } else { + quote! { + impl Crossover for #name { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::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")] diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index 3bb5bb9..bfb2dd7 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -11,7 +11,7 @@ impl RandomlyMutable for MyGenome { } } -impl CrossoverReproduction for MyGenome { +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., diff --git a/genetic-rs/examples/derive.rs b/genetic-rs/examples/derive.rs index d4d66e4..4148a88 100644 --- a/genetic-rs/examples/derive.rs +++ b/genetic-rs/examples/derive.rs @@ -12,7 +12,7 @@ impl RandomlyMutable for TestGene { } #[cfg(feature = "crossover")] -impl CrossoverReproduction for TestGene { +impl Crossover for TestGene { fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { Self { a: (self.a + other.a + rng.random_range(-0.5..0.5)) / 2., diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index 8fd126f..bad9d86 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -15,7 +15,7 @@ impl RandomlyMutable for MyGenome { impl DivisionReproduction for MyGenome {} -impl CrossoverReproduction for MyGenome { +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., From b09f9e788c87cca50bb59d9956a8a3efc720272e Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:41:24 +0000 Subject: [PATCH 14/28] fix macro compile errors --- genetic-rs-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 7c56860..4c12a1d 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -61,7 +61,7 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { let mut inner = Vec::new(); let mut tuple_struct = false; - for (i, field) in s.fields.iter().enumerate() { + for (i, field) in s.fields.into_iter().enumerate() { let ty = field.ty; let span = ty.span(); From 6f3b73bb1a5db3139e4737123295b331c95b6abf Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:53:01 +0000 Subject: [PATCH 15/28] rewrite the rest of hte macros --- genetic-rs-macros/src/lib.rs | 74 +++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 4c12a1d..da7fd8b 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput, Fields}; +use syn::{parse_macro_input, Data, DeriveInput}; use quote::quote_spanned; use syn::spanned::Spanned; @@ -10,32 +10,44 @@ use syn::spanned::Spanned; pub fn randmut_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let mut inner_mutate = quote!(); + let name = ast.ident; + + match ast.data { + Data::Struct(s) => { + let mut inner = Vec::new(); + + for (i, field) in s.fields.into_iter().enumerate() { + let ty = field.ty; + let span = ty.span(); - 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!(genetic_rs_common::prelude::RandomlyMutable::mutate(&mut self.#name, rate, 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 RandomlyMutable for an enum."); - } - let name = &ast.ident; - quote! { - impl genetic_rs_common::prelude::RandomlyMutable for #name { - fn mutate(&mut self, rate: f32, rng: &mut impl genetic_rs_common::Rng) { - #inner_mutate - } - } + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + + quote! { + impl genetic_rs_common::prelude::RandomlyMutable for #name { + fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) { + #inner + } + } + }.into() + }, + Data::Enum(_e) => { + panic!("enums not yet supported"); + }, + Data::Union(_u) => { + panic!("unions not yet supported"); + }, } - .into() } #[proc_macro_derive(DivisionReproduction)] @@ -44,7 +56,7 @@ pub fn mitosis_derive(input: TokenStream) -> TokenStream { let name = &ast.ident; quote! { - impl genetic_rs_common::prelude::DivisionReproduction for #name {} + impl genetic_rs_common::prelude::Mitosis for #name {} } .into() } @@ -67,12 +79,12 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { if let Some(field_name) = field.ident { inner.push(quote_spanned! {span=> - #field_name: <#ty as Crossover>::crossover(&self.#field_name, &other.#field_name, rate, rng), + #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 Crossover>::crossover(&self.#i, &other.#i, rate, rng), + <#ty as genetic_rs_common::prelude::Crossover>::crossover(&self.#i, &other.#i, rate, rng), }); } } @@ -81,7 +93,7 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { if tuple_struct { quote! { - impl Crossover for #name { + impl genetic_rs_common::prelude::Crossover for #name { fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::Rng) -> Self { Self(#inner) } @@ -89,7 +101,7 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { }.into() } else { quote! { - impl Crossover for #name { + impl genetic_rs_common::prelude::Crossover for #name { fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::Rng) -> Self { Self { #inner @@ -126,12 +138,12 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { if let Some(field_name) = field.ident { inner.push(quote_spanned! {span=> - #field_name: <#ty as GenerateRandom>::gen_random(rng), + #field_name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), }); } else { tuple_struct = true; inner.push(quote_spanned! {span=> - <#ty as GenerateRandom>::gen_random(rng), + <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), }); } } @@ -139,7 +151,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { let inner: proc_macro2::TokenStream = inner.into_iter().collect(); if tuple_struct { quote! { - impl GenerateRandom for #name { + impl genetic_rs_common::prelude::GenerateRandom for #name { fn gen_random(rng: &mut impl rand::Rng) -> Self { Self(#inner) } @@ -147,7 +159,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { }.into() } else { quote! { - impl GenerateRandom for #name { + impl genetic_rs_common::prelude::GenerateRandom for #name { fn gen_random(rng: &mut impl rand::Rng) -> Self { Self { #inner From 8b1acd217af77688406ec88b8606903c0665c074 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:53:54 +0000 Subject: [PATCH 16/28] cargo fmt --- genetic-rs-macros/src/lib.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index da7fd8b..32d3966 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -2,9 +2,9 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, Data, DeriveInput}; 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 { @@ -39,14 +39,15 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { #inner } } - }.into() - }, + } + .into() + } Data::Enum(_e) => { panic!("enums not yet supported"); - }, + } Data::Union(_u) => { panic!("unions not yet supported"); - }, + } } } @@ -110,13 +111,13 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { } }.into() } - }, + } Data::Enum(_e) => { panic!("enums not yet supported"); - }, + } Data::Union(_u) => { panic!("unions not yet supported"); - }, + } } } @@ -135,9 +136,9 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { 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=> + inner.push(quote_spanned! {span=> #field_name: <#ty as genetic_rs_common::prelude::GenerateRandom>::gen_random(rng), }); } else { @@ -156,7 +157,8 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { Self(#inner) } } - }.into() + } + .into() } else { quote! { impl genetic_rs_common::prelude::GenerateRandom for #name { @@ -166,12 +168,13 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { } } } - }.into() + } + .into() } - }, + } Data::Enum(_e) => { panic!("enums not yet supported"); - }, + } Data::Union(_u) => { panic!("unions not yet supported"); } From f0e9e75b8ff3506f9fa1a4c10fae9aaa551a4adf Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:56:19 +0000 Subject: [PATCH 17/28] fix compile error from removing dependency --- genetic-rs-common/src/lib.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index 8ef0e03..32db28b 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -138,19 +138,16 @@ where /// 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 target_size = genomes.len(); - let mut new_genomes = self.eliminator.eliminate(genomes); - self.repopulator.repopulate(&mut new_genomes, target_size); - new_genomes - }); + let genomes = std::mem::take(&mut self.genomes); + let target_size = genomes.len(); + self.genomes = self.eliminator.eliminate(genomes); + self.repopulator.repopulate(&mut self.genomes, target_size); #[cfg(feature = "tracing")] drop(enter); From f203d76b0848895ddf9d8eb7201aa20dfbb0c841 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:04:09 +0000 Subject: [PATCH 18/28] fix examples --- genetic-rs-macros/src/lib.rs | 12 ++++++------ genetic-rs/examples/derive.rs | 8 ++++---- genetic-rs/examples/readme_ex.rs | 8 ++++---- genetic-rs/examples/speciation.rs | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index 32d3966..acd3050 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -35,7 +35,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { quote! { impl genetic_rs_common::prelude::RandomlyMutable for #name { - fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) { + fn mutate(&mut self, rate: f32, rng: &mut impl genetic_rs_common::Rng) { #inner } } @@ -51,7 +51,7 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { } } -#[proc_macro_derive(DivisionReproduction)] +#[proc_macro_derive(Mitosis)] pub fn mitosis_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; @@ -95,7 +95,7 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { if tuple_struct { quote! { impl genetic_rs_common::prelude::Crossover for #name { - fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::Rng) -> Self { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { Self(#inner) } } @@ -103,7 +103,7 @@ pub fn crossover_derive(input: TokenStream) -> TokenStream { } else { quote! { impl genetic_rs_common::prelude::Crossover for #name { - fn crossover(&self, other: &Self, rate: f32, rng: &mut impl rand::Rng) -> Self { + fn crossover(&self, other: &Self, rate: f32, rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #inner } @@ -153,7 +153,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { if tuple_struct { quote! { impl genetic_rs_common::prelude::GenerateRandom for #name { - fn gen_random(rng: &mut impl rand::Rng) -> Self { + fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { Self(#inner) } } @@ -162,7 +162,7 @@ pub fn genrand_derive(input: TokenStream) -> TokenStream { } else { quote! { impl genetic_rs_common::prelude::GenerateRandom for #name { - fn gen_random(rng: &mut impl rand::Rng) -> Self { + fn gen_random(rng: &mut impl genetic_rs_common::Rng) -> Self { Self { #inner } diff --git a/genetic-rs/examples/derive.rs b/genetic-rs/examples/derive.rs index 4148a88..808360b 100644 --- a/genetic-rs/examples/derive.rs +++ b/genetic-rs/examples/derive.rs @@ -13,9 +13,9 @@ impl RandomlyMutable for TestGene { #[cfg(feature = "crossover")] impl Crossover for TestGene { - fn crossover(&self, other: &Self, rng: &mut impl Rng) -> Self { + 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., } } } @@ -29,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 eaa5d70..8da08b2 100644 --- a/genetic-rs/examples/readme_ex.rs +++ b/genetic-rs/examples/readme_ex.rs @@ -7,7 +7,7 @@ struct MyGenome { field1: f32, } -// required in all of the builtin repopulators 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; @@ -15,7 +15,7 @@ impl RandomlyMutable for MyGenome { } // use auto derives for the builtin nextgen functions to work with your genome. -impl DivisionReproduction for MyGenome {} +impl Mitosis for MyGenome {} // helper trait that allows us to use `Vec::gen_random` for the initial population. impl GenerateRandom for MyGenome { @@ -41,7 +41,7 @@ fn main() { // 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), FitnessEliminator::new_with_default(my_fitness_fn), - DivisionRepopulator::new(0.25), // 25% mutation rate + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) @@ -60,7 +60,7 @@ fn main() { // 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), FitnessEliminator::new_with_default(my_fitness_fn), - DivisionRepopulator::new(0.25), // 25% mutation rate + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index bad9d86..c4cbf5e 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -13,7 +13,7 @@ impl RandomlyMutable for MyGenome { } } -impl DivisionReproduction for MyGenome {} +impl Mitosis for MyGenome {} impl Crossover for MyGenome { fn crossover(&self, other: &Self, rate: f32, rng: &mut impl Rng) -> Self { From e9b94546a4562f97f8e7a609db4344e055f11a60 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:21:34 +0000 Subject: [PATCH 19/28] replace spaghetti code in crossover --- genetic-rs-common/src/builtin/repopulator.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 2757ac9..67ed6bc 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -143,17 +143,16 @@ where 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(); + let mut champs_cycle = champions.iter().enumerate().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 { - // TODO bad way to do this, but it works for now - parent2 = &champions[rng.random_range(0..champions.len() - 1)]; + 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!( From b0dad7392b75a7582860c80a621aa105ec975e94 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:57:20 +0000 Subject: [PATCH 20/28] define feature bounded traits --- genetic-rs-common/src/lib.rs | 85 ++++++++++-------------------------- 1 file changed, 24 insertions(+), 61 deletions(-) diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index 32db28b..a03c51f 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -45,27 +45,33 @@ pub trait Repopulator { fn repopulate(&self, genomes: &mut Vec, target_size: usize); } -/// 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. +/// 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, R: Repopulator> { - /// The current population of genomes - pub genomes: Vec, +pub trait FeatureBoundedEliminator: Eliminator {} +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedEliminator for T {} - /// The eliminator used to eliminate unfit genomes - pub eliminator: E, +#[cfg(feature = "rayon")] +pub trait FeatureBoundedEliminator: Eliminator + Send + Sync {} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedEliminator for T {} - /// The repopulator used to refill the population - pub repopulator: R, -} +/// 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 FeatureBoundedRepopulator: Repopulator {} +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedRepopulator for T {} -/// Rayon version of the [`GeneticSim`] struct #[cfg(feature = "rayon")] -pub struct GeneticSim< - G: Sized + Sync, - E: Eliminator + Send + Sync, - R: Repopulator + Send + Sync, -> { +pub trait FeatureBoundedRepopulator: Repopulator + Send + Sync {} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedRepopulator 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, @@ -76,12 +82,11 @@ pub struct GeneticSim< pub repopulator: R, } -#[cfg(not(feature = "rayon"))] impl GeneticSim where G: Sized, - E: Eliminator, - R: Repopulator, + 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. @@ -119,48 +124,6 @@ where } } -#[cfg(feature = "rayon")] -impl GeneticSim -where - G: Sized + Send + Sync, - E: Eliminator + Send + Sync, - R: Repopulator + Send + Sync, -{ - /// 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, eliminator: E, repopulator: R) -> Self { - Self { - genomes: starting_genomes, - eliminator, - repopulator, - } - } - - /// Uses the [`Eliminator`] and [`Repopulator`] provided in [`GeneticSim::new`] to create the next generation of genomes. - pub fn next_generation(&mut self) { - #[cfg(feature = "tracing")] - let span = span!(Level::TRACE, "next_generation"); - - #[cfg(feature = "tracing")] - let enter = span.enter(); - - let genomes = std::mem::take(&mut self.genomes); - let target_size = genomes.len(); - self.genomes = self.eliminator.eliminate(genomes); - self.repopulator.repopulate(&mut self.genomes, target_size); - - #[cfg(feature = "tracing")] - drop(enter); - } - - /// 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")))] From c97cc6a052a3a48353e8d2898ba915dc4eafbd92 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:49:45 +0000 Subject: [PATCH 21/28] implement feature bounding on repopulators and eliminators --- genetic-rs-common/src/builtin/eliminator.rs | 26 ++++++--- genetic-rs-common/src/builtin/repopulator.rs | 55 ++++++-------------- genetic-rs-common/src/lib.rs | 14 +++++ 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs index 32f8558..10f9ac3 100644 --- a/genetic-rs-common/src/builtin/eliminator.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -1,4 +1,5 @@ use crate::Eliminator; +use crate::FeatureBoundedGenome; #[cfg(feature = "rayon")] use rayon::prelude::*; @@ -19,8 +20,22 @@ where } } +/// 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: Sized + Send> { +pub struct FitnessEliminator, G: FeatureBoundedGenome> { /// The fitness function used to evaluate genomes. pub fitness_fn: F, @@ -30,11 +45,10 @@ pub struct FitnessEliminator, G: Sized + Send> { _marker: std::marker::PhantomData, } -// TODO only require `Send` and `Sync` for `F` when `rayon` is enabled` impl FitnessEliminator where - F: FitnessFn + Send + Sync, - G: Sized + Send + Sync, + 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. @@ -87,8 +101,8 @@ where impl Eliminator for FitnessEliminator where - F: FitnessFn + Send + Sync, - G: Sized + Send + Sync, + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, { #[cfg(not(feature = "rayon"))] fn eliminate(&self, genomes: Vec) -> Vec { diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 67ed6bc..deff214 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -6,35 +6,27 @@ use crate::{Repopulator, Rng}; use tracing::*; /// 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 +/// 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 Mitosis: Clone + RandomlyMutable { - /// 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 - } -} +pub trait FeatureBoundedRandomlyMutable: RandomlyMutable {} +#[cfg(not(feature = "tracing"))] +impl FeatureBoundedRandomlyMutable for T {} -/// Used in dividually-reproducing [`next_gen`]s #[cfg(feature = "tracing")] -pub trait Mitosis: Clone + RandomlyMutable + std::fmt::Debug { +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. - /// If it is simply returning a cloned and mutated version, consider using a constant mutation rate. fn divide(&self, rate: f32, rng: &mut impl Rng) -> Self { let mut child = self.clone(); child.mutate(rate, rng); @@ -45,7 +37,7 @@ pub trait Mitosis: Clone + RandomlyMutable + std::fmt::Debug { /// Used in crossover-reproducing [`next_gen`]s #[cfg(all(feature = "crossover", not(feature = "tracing")))] #[cfg_attr(docsrs, doc(cfg(feature = "crossover")))] -pub trait Crossover: Clone { +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; } @@ -59,7 +51,7 @@ pub trait Crossover: Clone + std::fmt::Debug { } /// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. -#[cfg(all(feature = "speciation", not(feature = "tracing")))] +#[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. @@ -71,19 +63,6 @@ pub trait Speciated: Sized { } } -/// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. -#[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() - } -} - /// Repopulator that uses division reproduction to create new genomes. pub struct MitosisRepopulator { /// The mutation rate to use when mutating genomes. 0.0 - 1.0 @@ -120,13 +99,13 @@ where } /// Repopulator that uses crossover reproduction to create new genomes. -pub struct CrossoverRepopulator { +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 { +impl CrossoverRepopulator { /// Creates a new [`CrossoverRepopulator`]. pub fn new(mutation_rate: f32) -> Self { Self { @@ -138,7 +117,7 @@ impl CrossoverRepopulator { impl Repopulator for CrossoverRepopulator where - G: Crossover + PartialEq, + G: Crossover, { fn repopulate(&self, genomes: &mut Vec, target_size: usize) { let mut rng = rand::rng(); diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index a03c51f..923918a 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -69,6 +69,20 @@ 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> { From a750201233bc9bebd69538b920528c7265d30c78 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:34:55 +0000 Subject: [PATCH 22/28] cargo fmt --- genetic-rs-common/src/builtin/repopulator.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index deff214..d31541a 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -23,7 +23,6 @@ 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. From c686812830e69e92f31b848ba4df66120df6ff21 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:38:51 +0000 Subject: [PATCH 23/28] add missing doc comments to fix clippy --- genetic-rs-common/src/builtin/repopulator.rs | 2 ++ genetic-rs-common/src/lib.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index d31541a..d183fdb 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -18,6 +18,8 @@ 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")] diff --git a/genetic-rs-common/src/lib.rs b/genetic-rs-common/src/lib.rs index 923918a..a3d2c8f 100644 --- a/genetic-rs-common/src/lib.rs +++ b/genetic-rs-common/src/lib.rs @@ -52,6 +52,8 @@ pub trait FeatureBoundedEliminator: Eliminator {} #[cfg(not(feature = "rayon"))] impl> FeatureBoundedEliminator 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 FeatureBoundedEliminator: Eliminator + Send + Sync {} #[cfg(feature = "rayon")] @@ -64,6 +66,8 @@ pub trait FeatureBoundedRepopulator: Repopulator {} #[cfg(not(feature = "rayon"))] impl> 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(feature = "rayon")] pub trait FeatureBoundedRepopulator: Repopulator + Send + Sync {} #[cfg(feature = "rayon")] From 99f92c71320addd3bfe6ea6da412d1a206c02eba Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:44:24 +0000 Subject: [PATCH 24/28] fix some doc comments --- genetic-rs-common/src/builtin/mod.rs | 4 ++-- genetic-rs-common/src/builtin/repopulator.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/genetic-rs-common/src/builtin/mod.rs b/genetic-rs-common/src/builtin/mod.rs index 12a5060..3e29be2 100644 --- a/genetic-rs-common/src/builtin/mod.rs +++ b/genetic-rs-common/src/builtin/mod.rs @@ -1,5 +1,5 @@ -/// Contains types implementing [`Eliminator`] +/// Contains types implementing [`Eliminator`][crate::Eliminator] pub mod eliminator; -/// Contains types implementing [`Repopulator`] +/// 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 index d183fdb..de0af12 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -5,7 +5,7 @@ use crate::{Repopulator, Rng}; #[cfg(feature = "tracing")] use tracing::*; -/// Used in all of the builtin [`next_gen`]s to randomly mutate genomes a given amount +/// 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); @@ -35,7 +35,7 @@ pub trait Mitosis: Clone + FeatureBoundedRandomlyMutable { } } -/// Used in crossover-reproducing [`next_gen`]s +/// 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 { From c4c862f3c2eadba81f0afb17e9f3fdd1e2ef28f8 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:02:50 +0000 Subject: [PATCH 25/28] update README --- README.md | 41 +++++---------- genetic-rs/examples/crossover.rs | 2 +- genetic-rs/src/lib.rs | 89 +------------------------------- 3 files changed, 16 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index fad756c..3884a5a 100644 --- a/README.md +++ b/README.md @@ -6,44 +6,31 @@ 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 + +First, define your genome type and impl the necessary traits: ```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. impl GenerateRandom for MyGenome { fn gen_random(rng: &mut impl rand::Rng) -> Self { @@ -52,7 +39,7 @@ impl GenerateRandom for MyGenome { } ``` -Once you have a struct, you must create your fitness function: +Once you have a struct, you should 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. @@ -72,8 +59,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, 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, + FitnessEliminator::new_with_default(my_fitness_fn), + MitosisRepopulator::new(0.25), // 25% mutation rate ); // perform evolution (100 gens) @@ -83,7 +70,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. You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/genetic-rs/examples/) for more complicated systems. ### License This project falls under the `MIT` license. diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index bfb2dd7..157a1d8 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -34,7 +34,7 @@ fn main() { let mut rng = rand::rng(); let magic_number = rng.random::() * 1000.; - let fitness = move |e: &MyGenome| -> f32 { -(magic_number - e.val).abs() }; + let fitness = move |e: &MyGenome| -> f32 { 1 / (magic_number - e.val).abs().max(1e-7) }; let mut sim = GeneticSim::new( Vec::gen_random(&mut rng, 100), 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::*; From 8375c4511d5be83ea194d92a98c9beb1a9423da0 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:09:27 +0000 Subject: [PATCH 26/28] fix doctest in README --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3884a5a..8afb754 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ First off, this crate comes with the `builtin`, `crossover`, and `genrand` featu > [!NOTE] > If you are interested in implementing NEAT with this, try out the [neat](https://crates.io/crates/neat) crate -First, define your genome type and impl the necessary traits: +Here's a simple genetic algorithm. ```rust use genetic_rs::prelude::*; @@ -31,34 +31,27 @@ impl RandomlyMutable for MyGenome { } } -// 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 { Self { field1: rng.gen() } } } -``` -Once you have a struct, you should 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), + // this is the `rayon` feature signature. + Vec::gen_random(100), FitnessEliminator::new_with_default(my_fitness_fn), MitosisRepopulator::new(0.25), // 25% mutation rate ); @@ -70,7 +63,7 @@ fn main() { } ``` -That is the minimal code for a working 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. From ac74e811842616b68aef706b0798b76ce7506792 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:10:26 +0000 Subject: [PATCH 27/28] remove incorrectly qualified path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8afb754..e43a28c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ impl RandomlyMutable for MyGenome { // 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() } } } From efe193a473d975c1925827288d7b688d4c6963f3 Mon Sep 17 00:00:00 2001 From: Tristan Murphy <72839119+HyperCodec@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:14:44 +0000 Subject: [PATCH 28/28] update version number and toml files --- Cargo.lock | 92 +++++--------------------------- Cargo.toml | 2 +- genetic-rs-common/Cargo.toml | 2 +- genetic-rs-macros/Cargo.toml | 2 +- genetic-rs/Cargo.toml | 8 +-- genetic-rs/examples/crossover.rs | 2 +- 6 files changed, 20 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3a201e..6dba5d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,25 +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", "tracing", ] [[package]] name = "genetic-rs-macros" -version = "0.6.0" +version = "1.0.0" dependencies = [ "genetic-rs-common", "proc-macro2", @@ -73,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" @@ -93,7 +82,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi", ] [[package]] @@ -146,34 +135,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -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" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -183,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]] @@ -201,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]] @@ -272,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" @@ -295,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 484c588..75f3645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ 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" diff --git a/genetic-rs-common/Cargo.toml b/genetic-rs-common/Cargo.toml index a41f7e4..f7f92d3 100644 --- a/genetic-rs-common/Cargo.toml +++ b/genetic-rs-common/Cargo.toml @@ -27,6 +27,6 @@ features = ["crossover", "speciation"] rustdoc-args = ["--cfg", "docsrs"] [dependencies] -rand = { version = "0.9.0", optional = true } +rand = { version = "0.9.2", optional = true } rayon = { version = "1.10.0", optional = true } tracing = { version = "0.1.41", optional = true } 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/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 157a1d8..872ae39 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -34,7 +34,7 @@ fn main() { let mut rng = rand::rng(); let magic_number = rng.random::() * 1000.; - let fitness = move |e: &MyGenome| -> f32 { 1 / (magic_number - e.val).abs().max(1e-7) }; + 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),