genetic_rs_common/lib.rs
1#![warn(missing_docs)]
2#![allow(clippy::needless_doctest_main)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5//! The crate containing the core traits and structs of genetic-rs.
6
7use replace_with::replace_with_or_abort;
8
9/// Built-in nextgen functions and traits to go with them.
10#[cfg_attr(docsrs, doc(cfg(feature = "builtin")))]
11#[cfg(feature = "builtin")]
12pub mod builtin;
13
14/// Used to quickly import everything this crate has to offer.
15/// Simply add `use genetic_rs::prelude::*` to begin using this crate.
16pub mod prelude;
17
18#[cfg(feature = "rayon")]
19use rayon::prelude::*;
20
21/// Represents a fitness function. Inputs a reference to the genome and outputs an f32.
22pub trait FitnessFn<G> {
23 /// Evaluates a genome's fitness
24 fn fitness(&self, genome: &G) -> f32;
25}
26
27impl<F: Fn(&G) -> f32, G> FitnessFn<G> for F {
28 fn fitness(&self, genome: &G) -> f32 {
29 (self)(genome)
30 }
31}
32
33/// Represents a nextgen function. Inputs genomes and rewards and produces the next generation
34pub trait NextgenFn<G> {
35 /// Creates the next generation from the current fitness values.
36 fn next_gen(&self, fitness: Vec<(G, f32)>) -> Vec<G>;
37}
38
39impl<F: Fn(Vec<(G, f32)>) -> Vec<G>, G> NextgenFn<G> for F {
40 fn next_gen(&self, fitness: Vec<(G, f32)>) -> Vec<G> {
41 (self)(fitness)
42 }
43}
44
45/// The simulation controller.
46/// ```rust
47/// use genetic_rs_common::prelude::*;
48///
49/// #[derive(Debug, Clone)]
50/// struct MyGenome {
51/// a: f32,
52/// b: f32,
53/// }
54///
55/// impl RandomlyMutable for MyGenome {
56/// fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) {
57/// self.a += rng.gen::<f32>() * rate;
58/// self.b += rng.gen::<f32>() * rate;
59/// }
60/// }
61///
62/// impl DivisionReproduction for MyGenome {
63/// fn divide(&self, rng: &mut impl rand::Rng) -> Self {
64/// let mut child = self.clone();
65/// child.mutate(0.25, rng); // you'll generally want to use a constant mutation rate for mutating children.
66/// child
67/// }
68/// }
69///
70/// 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.
71///
72/// impl GenerateRandom for MyGenome {
73/// fn gen_random(rng: &mut impl rand::Rng) -> Self {
74/// Self {
75/// a: rng.gen(),
76/// b: rng.gen(),
77/// }
78/// }
79/// }
80///
81/// fn main() {
82/// let my_fitness_fn = |e: &MyGenome| {
83/// e.a * e.b // should result in genomes increasing their value
84/// };
85///
86/// let mut rng = rand::thread_rng();
87///
88/// let mut sim = GeneticSim::new(
89/// Vec::gen_random(&mut rng, 1000),
90/// my_fitness_fn,
91/// division_pruning_nextgen,
92/// );
93///
94/// for _ in 0..100 {
95/// // if this were a more complex simulation, you might test genomes in `sim.genomes` between `next_generation` calls to provide a more accurate reward.
96/// sim.next_generation();
97/// }
98///
99/// dbg!(sim.genomes);
100/// }
101/// ```
102#[cfg(not(feature = "rayon"))]
103pub struct GeneticSim<F, NG, G>
104where
105 F: FitnessFn<G>,
106 NG: NextgenFn<G>,
107 G: Sized,
108{
109 /// The current population of genomes
110 pub genomes: Vec<G>,
111 fitness: F,
112 next_gen: NG,
113}
114
115/// Rayon version of the [`GeneticSim`] struct
116#[cfg(feature = "rayon")]
117pub struct GeneticSim<F, NG, G>
118where
119 F: FitnessFn<G> + Send + Sync,
120 NG: NextgenFn<G> + Send + Sync,
121 G: Sized + Send,
122{
123 /// The current population of genomes
124 pub genomes: Vec<G>,
125 fitness: F,
126 next_gen: NG,
127}
128
129#[cfg(not(feature = "rayon"))]
130impl<F, NG, G> GeneticSim<F, NG, G>
131where
132 F: FitnessFn<G>,
133 NG: NextgenFn<G>,
134 G: Sized,
135{
136 /// Creates a [`GeneticSim`] with a given population of `starting_genomes` (the size of which will be retained),
137 /// a given fitness function, and a given nextgen function.
138 pub fn new(starting_genomes: Vec<G>, fitness: F, next_gen: NG) -> Self {
139 Self {
140 genomes: starting_genomes,
141 fitness,
142 next_gen,
143 }
144 }
145
146 /// Uses the `next_gen` provided in [`GeneticSim::new`] to create the next generation of genomes.
147 pub fn next_generation(&mut self) {
148 // TODO maybe remove unneccessary dependency, can prob use std::mem::replace
149 replace_with_or_abort(&mut self.genomes, |genomes| {
150 let rewards = genomes
151 .into_iter()
152 .map(|e| {
153 let fitness: f32 = self.fitness.fitness(&e);
154 (e, fitness)
155 })
156 .collect();
157
158 self.next_gen.next_gen(rewards)
159 });
160 }
161
162 /// Calls [`next_generation`][GeneticSim::next_generation] `count` number of times.
163 pub fn perform_generations(&mut self, count: usize) {
164 for _ in 0..count {
165 self.next_generation();
166 }
167 }
168}
169
170#[cfg(feature = "rayon")]
171impl<F, NG, G> GeneticSim<F, NG, G>
172where
173 F: FitnessFn<G> + Send + Sync,
174 NG: NextgenFn<G> + Send + Sync,
175 G: Sized + Send,
176{
177 /// Creates a [`GeneticSim`] with a given population of `starting_genomes` (the size of which will be retained),
178 /// a given fitness function, and a given nextgen function.
179 pub fn new(starting_genomes: Vec<G>, fitness: F, next_gen: NG) -> Self {
180 Self {
181 genomes: starting_genomes,
182 fitness,
183 next_gen,
184 }
185 }
186
187 /// Performs selection and produces the next generation within the simulation.
188 pub fn next_generation(&mut self) {
189 replace_with_or_abort(&mut self.genomes, |genomes| {
190 let rewards = genomes
191 .into_par_iter()
192 .map(|e| {
193 let fitness: f32 = self.fitness.fitness(&e);
194 (e, fitness)
195 })
196 .collect();
197
198 self.next_gen.next_gen(rewards)
199 });
200 }
201
202 /// Calls [`next_generation`][GeneticSim::next_generation] `count` number of times.
203 pub fn perform_generations(&mut self, count: usize) {
204 for _ in 0..count {
205 self.next_generation();
206 }
207 }
208}
209
210#[cfg(feature = "genrand")]
211use rand::prelude::*;
212
213/// Helper trait used in the generation of random starting populations
214#[cfg(feature = "genrand")]
215#[cfg_attr(docsrs, doc(cfg(feature = "genrand")))]
216pub trait GenerateRandom {
217 /// Create a completely random instance of the genome
218 fn gen_random(rng: &mut impl Rng) -> Self;
219}
220
221/// Blanket trait used on collections that contain objects implementing [`GenerateRandom`]
222#[cfg(all(feature = "genrand", not(feature = "rayon")))]
223#[cfg_attr(docsrs, doc(cfg(feature = "genrand")))]
224pub trait GenerateRandomCollection<T>
225where
226 T: GenerateRandom,
227{
228 /// Generate a random collection of the inner objects with a given amount
229 fn gen_random(rng: &mut impl Rng, amount: usize) -> Self;
230}
231
232/// Rayon version of the [`GenerateRandomCollection`] trait
233#[cfg(all(feature = "genrand", feature = "rayon"))]
234pub trait GenerateRandomCollection<T>
235where
236 T: GenerateRandom + Send,
237{
238 /// Generate a random collection of the inner objects with the given amount. Does not pass in rng like the sync counterpart.
239 fn gen_random(amount: usize) -> Self;
240}
241
242#[cfg(not(feature = "rayon"))]
243impl<C, T> GenerateRandomCollection<T> for C
244where
245 C: FromIterator<T>,
246 T: GenerateRandom,
247{
248 fn gen_random(rng: &mut impl Rng, amount: usize) -> Self {
249 (0..amount).map(|_| T::gen_random(rng)).collect()
250 }
251}
252
253#[cfg(feature = "rayon")]
254impl<C, T> GenerateRandomCollection<T> for C
255where
256 C: FromParallelIterator<T>,
257 T: GenerateRandom + Send,
258{
259 fn gen_random(amount: usize) -> Self {
260 (0..amount)
261 .into_par_iter()
262 .map(|_| T::gen_random(&mut rand::thread_rng()))
263 .collect()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::prelude::*;
270
271 #[test]
272 fn send_sim() {
273 let mut sim = GeneticSim::new(vec![()], |_: &()| 0., |_: Vec<((), f32)>| vec![()]);
274
275 let h = std::thread::spawn(move || {
276 sim.next_generation();
277 });
278
279 h.join().unwrap();
280 }
281}