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