diff --git a/Cargo.lock b/Cargo.lock index 8f0cfca..b20cd23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "bitflags" version = "2.9.0" @@ -14,6 +20,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -79,9 +105,21 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "genetic-rs" -version = "1.1.0" +version = "1.2.0" dependencies = [ "genetic-rs-common", "genetic-rs-macros", @@ -90,7 +128,7 @@ dependencies = [ [[package]] name = "genetic-rs-common" -version = "1.1.0" +version = "1.2.0" dependencies = [ "itertools", "rand", @@ -99,7 +137,7 @@ dependencies = [ [[package]] name = "genetic-rs-macros" -version = "1.1.0" +version = "1.2.0" dependencies = [ "darling", "genetic-rs-common", @@ -110,22 +148,63 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "itertools" version = "0.14.0" @@ -135,6 +214,18 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.171" @@ -142,10 +233,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] [[package]] name = "proc-macro2" @@ -173,32 +280,20 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom", -] +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rayon" @@ -220,6 +315,54 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "strsim" version = "0.11.1" @@ -244,19 +387,153 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 797ea76..636230e 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 = "1.1.0" +version = "1.2.0" authors = ["HyperCodec"] homepage = "https://github.com/hypercodec/genetic-rs" repository = "https://github.com/hypercodec/genetic-rs" diff --git a/README.md b/README.md index 4c90a04..89644b8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ use genetic_rs::prelude::*; // `Mitosis` can be derived if both `Clone` and `RandomlyMutable` are present. #[derive(Clone, Debug, Mitosis)] +#[mitosis(use_randmut = true)] struct MyGenome { field1: f32, } @@ -53,7 +54,7 @@ fn main() { // size will be preserved in builtin repopulators, but it is not required to keep a constant size if you were to build your own. // 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), - FitnessEliminator::new_with_default(my_fitness_fn), + FitnessEliminator::new_without_observer(my_fitness_fn), MitosisRepopulator::new(0.25, ()), // 25% mutation rate, empty context ); diff --git a/genetic-rs-common/Cargo.toml b/genetic-rs-common/Cargo.toml index 419adaa..f497444 100644 --- a/genetic-rs-common/Cargo.toml +++ b/genetic-rs-common/Cargo.toml @@ -28,5 +28,5 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] itertools = { version = "0.14.0", optional = true } -rand = { version = "0.9.2", optional = true } +rand = { version = "0.10.0", optional = true } rayon = { version = "1.11.0", optional = true } diff --git a/genetic-rs-common/src/builtin/eliminator.rs b/genetic-rs-common/src/builtin/eliminator.rs index 108a7d4..a64bcb7 100644 --- a/genetic-rs-common/src/builtin/eliminator.rs +++ b/genetic-rs-common/src/builtin/eliminator.rs @@ -34,38 +34,79 @@ pub trait FeatureBoundedFitnessFn: FitnessFn + Send #[cfg(feature = "rayon")] impl + Send + Sync> FeatureBoundedFitnessFn for T {} +/// A trait for observing fitness scores. This can be used to implement things like logging or statistics collection. +pub trait FitnessObserver { + /// Observes the fitness scores of a generation of genomes. + fn observe(&self, fitnesses: &[(G, f32)]); +} + +impl FitnessObserver for () { + fn observe(&self, _fitnesses: &[(G, f32)]) {} +} + +#[cfg(not(feature = "rayon"))] +#[doc(hidden)] +pub trait FeatureBoundedFitnessObserver: FitnessObserver {} + +#[cfg(not(feature = "rayon"))] +impl> FeatureBoundedFitnessObserver for T {} + +#[cfg(feature = "rayon")] +#[doc(hidden)] +pub trait FeatureBoundedFitnessObserver: + FitnessObserver + Send + Sync +{ +} +#[cfg(feature = "rayon")] +impl + Send + Sync> FeatureBoundedFitnessObserver + for T +{ +} + /// A fitness-based eliminator that eliminates genomes based on their fitness scores. -pub struct FitnessEliminator, G: FeatureBoundedGenome> { +pub struct FitnessEliminator< + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver = (), +> { /// The fitness function used to evaluate genomes. pub fitness_fn: F, /// The percentage of genomes to keep. Must be between 0.0 and 1.0. pub threshold: f32, + /// The fitness observer used to observe fitness scores. + pub observer: O, + _marker: std::marker::PhantomData, } -impl FitnessEliminator +impl FitnessEliminator where F: FeatureBoundedFitnessFn, G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver, { + /// The default threshold for the [`FitnessEliminator`]. This is the percentage of genomes to keep. All genomes below the median fitness will be eliminated. + pub const DEFAULT_THRESHOLD: f32 = 0.5; + /// 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 { + pub fn new(fitness_fn: F, threshold: f32, observer: O) -> Self { if !(0.0..=1.0).contains(&threshold) { panic!("Threshold must be between 0.0 and 1.0"); } Self { fitness_fn, threshold, + observer, _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) + pub fn new_with_default_threshold(fitness_fn: F, observer: O) -> Self { + Self::new(fitness_fn, Self::DEFAULT_THRESHOLD, observer) } /// Calculates the fitness of each genome and sorts them by fitness. @@ -97,18 +138,56 @@ where fitnesses.sort_by(|(_a, afit), (_b, bfit)| bfit.partial_cmp(afit).unwrap()); fitnesses } + + /// Creates a new builder for [`FitnessEliminator`] to make it easier to construct with default parameters. + pub fn builder() -> FitnessEliminatorBuilder { + FitnessEliminatorBuilder::default() + } +} + +impl FitnessEliminator +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver + Default, +{ + /// Creates a new [`FitnessEliminator`] with a default observer that does nothing. + pub fn new_with_default_observer(fitness_fn: F, threshold: f32) -> Self { + Self::new(fitness_fn, threshold, O::default()) + } + + /// Creates a new [`FitnessEliminator`] with a default threshold of 0.5 and a default observer. + /// You must specify the observer type explicitly, e.g., `FitnessEliminator::new_with_default::<()>(fitness_fn)`. + pub fn new_with_default(fitness_fn: F) -> Self { + Self::new_with_default_observer(fitness_fn, Self::DEFAULT_THRESHOLD) + } } -impl Eliminator for FitnessEliminator +/// Implementation specifically for the unit type `()` observer (the default). +impl FitnessEliminator where F: FeatureBoundedFitnessFn, G: FeatureBoundedGenome, +{ + /// Creates a new [`FitnessEliminator`] with a default threshold of 0.5 and unit observer `()`. + /// This is a convenience function that doesn't require explicit type annotations. + pub fn new_without_observer(fitness_fn: F) -> Self { + Self::new(fitness_fn, Self::DEFAULT_THRESHOLD, ()) + } +} + +impl Eliminator for FitnessEliminator +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver, { #[cfg(not(feature = "rayon"))] fn eliminate(&self, genomes: Vec) -> Vec { let mut fitnesses = self.calculate_and_sort(genomes); let median_index = (fitnesses.len() as f32) * self.threshold; fitnesses.truncate(median_index as usize + 1); + self.observer.observe(&fitnesses); fitnesses.into_iter().map(|(g, _)| g).collect() } @@ -117,14 +196,92 @@ where let mut fitnesses = self.calculate_and_sort(genomes); let median_index = (fitnesses.len() as f32) * self.threshold; fitnesses.truncate(median_index as usize + 1); + self.observer.observe(&fitnesses); fitnesses.into_par_iter().map(|(g, _)| g).collect() } } +/// A builder for [`FitnessEliminator`] to make it easier to construct with default parameters. +pub struct FitnessEliminatorBuilder, G, O: FitnessObserver = ()> { + fitness_fn: Option, + threshold: f32, + observer: Option, + _marker: std::marker::PhantomData, +} + +impl FitnessEliminatorBuilder +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver, +{ + /// Sets the fitness function for the [`FitnessEliminator`]. + pub fn fitness_fn(mut self, fitness_fn: F) -> Self { + self.fitness_fn = Some(fitness_fn); + self + } + + /// Sets the threshold for the [`FitnessEliminator`]. + pub fn threshold(mut self, threshold: f32) -> Self { + self.threshold = threshold; + self + } + + /// Sets the observer for the [`FitnessEliminator`]. + pub fn observer(mut self, observer: O) -> Self { + self.observer = Some(observer); + self + } + + /// Builds the [`FitnessEliminator`]. + /// Panics if the fitness function or observer was not set. + pub fn build_or_panic(self) -> FitnessEliminator { + let fitness_fn = self.fitness_fn.expect("Fitness function must be set"); + let observer = self.observer.expect( + "Observer must be set. Use build_or_default() if the observer implements Default.", + ); + FitnessEliminator::new(fitness_fn, self.threshold, observer) + } +} + +impl FitnessEliminatorBuilder +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver + Default, +{ + /// Builds the [`FitnessEliminator`]. + /// If no observer was set, uses the default observer implementation. + /// This method is only available when the observer type implements [`Default`]. + pub fn build(self) -> FitnessEliminator { + let fitness_fn = self.fitness_fn.expect("Fitness function must be set"); + let observer = self.observer.unwrap_or_default(); + FitnessEliminator::new(fitness_fn, self.threshold, observer) + } +} + +impl Default for FitnessEliminatorBuilder +where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + O: FeatureBoundedFitnessObserver, +{ + fn default() -> Self { + Self { + fitness_fn: None, + threshold: 0.5, + observer: None, + _marker: std::marker::PhantomData, + } + } +} + #[cfg(feature = "knockout")] mod knockout { use std::cmp::Ordering; + use rand::RngExt; + use super::*; /// A distinct type to help clarify the result of a knockout function. @@ -183,6 +340,35 @@ mod knockout { } } + /// A knockout function that uses a fitness function to determine the winner. + pub struct FitnessKnockoutFn, G: FeatureBoundedGenome> { + /// The fitness function used to evaluate the genomes. + pub fitness_fn: F, + _marker: std::marker::PhantomData, + } + + impl, G: FeatureBoundedGenome> FitnessKnockoutFn { + /// Creates a new [`FitnessKnockoutFn`] with a given fitness function. + pub fn new(fitness_fn: F) -> Self { + Self { + fitness_fn, + _marker: std::marker::PhantomData, + } + } + } + + impl KnockoutFn for FitnessKnockoutFn + where + F: FeatureBoundedFitnessFn, + G: FeatureBoundedGenome, + { + fn knockout(&self, a: &G, b: &G) -> KnockoutWinner { + let afit = self.fitness_fn.fitness(a); + let bfit = self.fitness_fn.fitness(b); + afit.total_cmp(&bfit).into() + } + } + #[doc(hidden)] #[cfg(not(feature = "rayon"))] pub trait FeatureBoundedKnockoutFn: KnockoutFn {} diff --git a/genetic-rs-common/src/builtin/repopulator.rs b/genetic-rs-common/src/builtin/repopulator.rs index 04e690a..2ddd7ac 100644 --- a/genetic-rs-common/src/builtin/repopulator.rs +++ b/genetic-rs-common/src/builtin/repopulator.rs @@ -1,5 +1,4 @@ use crate::Repopulator; -use rand::Rng as RandRng; /// Used in other traits to randomly mutate genomes a given amount pub trait RandomlyMutable { @@ -10,16 +9,42 @@ pub trait RandomlyMutable { fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng); } +// TODO rayon version +impl<'a, T: RandomlyMutable + 'a, I: Iterator> RandomlyMutable for I { + type Context = T::Context; + + fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) { + self.for_each(|x| x.mutate(ctx, rate, rng)); + } +} + /// Used in dividually-reproducing [`Repopulator`]s pub trait Mitosis: Clone { /// Simulation-wide context required for this mitosis implementation. type Context; - /// Create a new child with mutation. Similar to [RandomlyMutable::mutate], but returns a new instance instead of modifying the original. + /// Create a new child with mutation. Similar to [`RandomlyMutable::mutate`], but returns a new instance instead of modifying the original. fn divide(&self, ctx: &::Context, rate: f32, rng: &mut impl rand::Rng) -> Self; } +impl Mitosis for Vec { + type Context = T::Context; + + fn divide( + &self, + ctx: &::Context, + rate: f32, + rng: &mut impl rand::Rng, + ) -> Self { + let mut child = Vec::with_capacity(self.len()); + for gene in self { + child.push(gene.divide(ctx, rate, rng)); + } + child + } +} + /// Repopulator that uses division reproduction to create new genomes. pub struct MitosisRepopulator { /// The mutation rate to use when mutating genomes. 0.0 - 1.0 @@ -61,6 +86,8 @@ where #[cfg(feature = "crossover")] mod crossover { + use rand::RngExt; + use super::*; /// Used in crossover-reproducing [`Repopulator`]s @@ -134,6 +161,8 @@ pub use crossover::*; mod speciation { use std::collections::HashMap; + use rand::RngExt; + use super::*; /// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different. diff --git a/genetic-rs-macros/Cargo.toml b/genetic-rs-macros/Cargo.toml index df5331c..cab11dc 100644 --- a/genetic-rs-macros/Cargo.toml +++ b/genetic-rs-macros/Cargo.toml @@ -23,7 +23,7 @@ genrand = [] [dependencies] darling = "0.23.0" -genetic-rs-common = { path = "../genetic-rs-common", version = "1.1.0" } +genetic-rs-common = { path = "../genetic-rs-common", version = "1.2.0" } proc-macro2 = "1.0.106" quote = "1.0.44" syn = "2.0.114" diff --git a/genetic-rs-macros/src/lib.rs b/genetic-rs-macros/src/lib.rs index e679a6d..b7ed25f 100644 --- a/genetic-rs-macros/src/lib.rs +++ b/genetic-rs-macros/src/lib.rs @@ -1,6 +1,8 @@ extern crate proc_macro; +use darling::util::PathList; use darling::FromAttributes; +use darling::FromMeta; use proc_macro::TokenStream; use quote::quote; use quote::quote_spanned; @@ -13,7 +15,8 @@ use syn::{parse_macro_input, Data, DeriveInput}; pub fn randmut_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let (def_ctx, mut ctx_ident) = create_context_helper(&ast, parse_quote!(RandomlyMutable)); + let (def_ctx, mut ctx_ident) = + create_context_helper(&ast, parse_quote!(RandomlyMutable), parse_quote!(randmut)); let custom_context = ctx_ident.is_some(); let name = ast.ident; @@ -78,53 +81,170 @@ pub fn randmut_derive(input: TokenStream) -> TokenStream { } } -#[proc_macro_derive(Mitosis)] +#[derive(FromAttributes)] +#[darling(attributes(mitosis))] +struct MitosisSettings { + use_randmut: Option, + + // darling is annoyingly restrictive and doesn't + // let me just ignore extra fields + #[darling(rename = "create_context")] + _create_context: Option, + + #[darling(rename = "with_context")] + _with_context: Option, +} + +#[proc_macro_derive(Mitosis, attributes(mitosis))] pub fn mitosis_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; - quote! { - #[automatically_derived] - impl genetic_rs_common::prelude::Mitosis for #name { - type Context = ::Context; + let mitosis_settings = MitosisSettings::from_attributes(&ast.attrs).unwrap(); + if mitosis_settings.use_randmut.is_some() && mitosis_settings.use_randmut.unwrap() { + quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::Mitosis for #name { + type Context = ::Context; + + fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { + let mut child = self.clone(); + ::mutate(&mut child, ctx, rate, rng); + child + } + } + } + .into() + } else { + let (def_ctx, mut ctx_ident) = + create_context_helper(&ast, parse_quote!(RandomlyMutable), parse_quote!(mitosis)); + let custom_context = ctx_ident.is_some(); + + let name = ast.ident; + + match ast.data { + Data::Struct(s) => { + let mut is_tuple_struct = false; + let mut inner = Vec::new(); + + for (i, field) in s.fields.into_iter().enumerate() { + let ty = field.ty; + let span = ty.span(); - fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { - let mut child = self.clone(); - ::mutate(&mut child, ctx, rate, rng); - child + if ctx_ident.is_none() { + ctx_ident = Some( + quote_spanned! {span=> <#ty as genetic_rs_common::prelude::Mitosis>::Context }, + ); + } + + if let Some(field_name) = field.ident { + if custom_context { + inner.push(quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, &ctx.#field_name, rate, rng), + }); + } else { + inner.push(quote_spanned! {span=> + #field_name: <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#field_name, ctx, rate, rng), + }); + } + } else if custom_context { + is_tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#i, &ctx.#i, rate, rng), + }); + } else { + is_tuple_struct = true; + inner.push(quote_spanned! {span=> + <#ty as genetic_rs_common::prelude::Mitosis>::divide(&self.#i, ctx, rate, rng), + }); + } + } + + let inner: proc_macro2::TokenStream = inner.into_iter().collect(); + let child = if is_tuple_struct { + quote! { + Self(#inner) + } + } else { + quote! { + Self { + #inner + } + } + }; + + quote! { + #[automatically_derived] + impl genetic_rs_common::prelude::Mitosis for #name { + type Context = #ctx_ident; + + fn divide(&self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) -> Self { + #child + } + } + + #def_ctx + } + .into() + } + Data::Enum(_e) => { + panic!("enums not yet supported"); + } + Data::Union(_u) => { + panic!("unions not yet supported"); } } } - .into() } -#[derive(FromAttributes)] -#[darling(attributes(randmut, crossover))] +#[derive(FromMeta)] struct ContextArgs { with_context: Option, - create_context: Option, + create_context: Option, +} + +#[derive(FromMeta)] +struct CreateContext { + name: syn::Ident, + derive: Option, } fn create_context_helper( ast: &DeriveInput, trait_name: syn::Ident, + attr_path: syn::Path, ) -> ( Option, Option, ) { let name = &ast.ident; - let doc = quote! { #[doc = concat!("Autogenerated context struct for ", stringify!(#name))] }; + let doc = + quote! { #[doc = concat!("Autogenerated context struct for [`", stringify!(#name), "`]")] }; let vis = ast.vis.to_token_stream(); - let args = ContextArgs::from_attributes(&ast.attrs).unwrap(); + let attr = ast.attrs.iter().find(|a| a.path() == &attr_path); + if attr.is_none() { + return (None, None); + } + + let meta = &attr.unwrap().meta; + + let args = ContextArgs::from_meta(meta).unwrap(); if args.create_context.is_some() && args.with_context.is_some() { panic!("cannot have both create_context and with_context"); } - if let Some(ident) = args.create_context { + if let Some(create_ctx) = args.create_context { + let ident = &create_ctx.name; + let derives = create_ctx.derive.map(|paths| { + quote! { + #[derive(#(#paths,)*)] + } + }); + match &ast.data { Data::Struct(s) => { let mut inner = Vec::::new(); @@ -150,13 +270,13 @@ fn create_context_helper( if tuple_struct { return ( - Some(quote! { #doc #vis struct #ident (#inner);}), + Some(quote! { #doc #derives #vis struct #ident (#inner);}), Some(ident.to_token_stream()), ); } return ( - Some(quote! { #doc #vis struct #ident {#inner};}), + Some(quote! { #doc #derives #vis struct #ident {#inner};}), Some(ident.to_token_stream()), ); } @@ -177,7 +297,8 @@ fn create_context_helper( pub fn crossover_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - let (def_ctx, mut context) = create_context_helper(&ast, parse_quote!(Crossover)); + let (def_ctx, mut context) = + create_context_helper(&ast, parse_quote!(Crossover), parse_quote!(crossover)); let custom_context = context.is_some(); let name = ast.ident; diff --git a/genetic-rs/Cargo.toml b/genetic-rs/Cargo.toml index 729d3a4..2b211d8 100644 --- a/genetic-rs/Cargo.toml +++ b/genetic-rs/Cargo.toml @@ -22,14 +22,14 @@ rayon = ["genetic-rs-common/rayon"] derive = ["dep:genetic-rs-macros", "builtin"] [dependencies] -genetic-rs-common = { path = "../genetic-rs-common", version = "1.1.0", default-features = false } -genetic-rs-macros = { path = "../genetic-rs-macros", version = "1.1.0", optional = true } +genetic-rs-common = { path = "../genetic-rs-common", version = "1.2.0", default-features = false } +genetic-rs-macros = { path = "../genetic-rs-macros", version = "1.2.0", optional = true } [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ["cfg(publish)"] } [dev-dependencies] -rand = "0.9.2" +rand = "0.10.0" [[example]] name = "crossover" diff --git a/genetic-rs/examples/crossover.rs b/genetic-rs/examples/crossover.rs index 81af7df..800fb05 100644 --- a/genetic-rs/examples/crossover.rs +++ b/genetic-rs/examples/crossover.rs @@ -41,7 +41,7 @@ fn main() { let mut sim = GeneticSim::new( Vec::gen_random(&mut rng, 100), - FitnessEliminator::new_with_default(fitness), + FitnessEliminator::new_without_observer(fitness), CrossoverRepopulator::new(0.25, ()), // 25% mutation rate ); diff --git a/genetic-rs/examples/derive.rs b/genetic-rs/examples/derive.rs index 885a728..7c79d9e 100644 --- a/genetic-rs/examples/derive.rs +++ b/genetic-rs/examples/derive.rs @@ -1,9 +1,15 @@ +#![allow(dead_code)] + use genetic_rs::prelude::*; +#[derive(Clone, Debug, Default)] struct Context1; + +#[derive(Clone, Debug, Default)] struct Context2; #[derive(Clone, Mitosis, Debug)] +#[mitosis(use_randmut = true)] struct Foo1(f32); impl RandomlyMutable for Foo1 { @@ -15,6 +21,7 @@ impl RandomlyMutable for Foo1 { } #[derive(Clone, Mitosis, Debug)] +#[mitosis(use_randmut = true)] struct Foo2(f32); impl RandomlyMutable for Foo2 { @@ -26,7 +33,8 @@ impl RandomlyMutable for Foo2 { } #[derive(Clone, RandomlyMutable, Mitosis, Debug)] -#[randmut(create_context = BarCtx)] +#[randmut(create_context(name = BarCtx, derive(Clone, Debug, Default)))] +#[mitosis(with_context = BarCtx)] struct Bar { a: Foo1, b: Foo2, diff --git a/genetic-rs/examples/readme_ex.rs b/genetic-rs/examples/readme_ex.rs index ca0680a..e8ec777 100644 --- a/genetic-rs/examples/readme_ex.rs +++ b/genetic-rs/examples/readme_ex.rs @@ -49,7 +49,7 @@ 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), - FitnessEliminator::new_with_default(my_fitness_fn), + FitnessEliminator::new_without_observer(my_fitness_fn), MitosisRepopulator::new(0.25, ()), // 25% mutation rate ); diff --git a/genetic-rs/examples/speciation.rs b/genetic-rs/examples/speciation.rs index b55c5e9..662ee45 100644 --- a/genetic-rs/examples/speciation.rs +++ b/genetic-rs/examples/speciation.rs @@ -68,7 +68,7 @@ fn main() { let mut sim = GeneticSim::new( Vec::gen_random(&mut rng, 100), - FitnessEliminator::new_with_default(fitness), + FitnessEliminator::new_without_observer(fitness), // 25% mutation rate, allow cross-species reproduction in emergency scenarios SpeciatedCrossoverRepopulator::new(0.25, true, ()), );