Skip to main content

genetic_rs_common/builtin/
repopulator.rs

1use crate::Repopulator;
2
3/// Used in other traits to randomly mutate genomes a given amount
4pub trait RandomlyMutable {
5    /// Simulation-wide context required for this mutation implementation.
6    type Context;
7
8    /// Mutate the genome with a given mutation rate (0..1)
9    fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng);
10}
11
12// TODO rayon version
13impl<'a, T: RandomlyMutable + 'a, I: Iterator<Item = &'a mut T>> RandomlyMutable for I {
14    type Context = T::Context;
15
16    fn mutate(&mut self, ctx: &Self::Context, rate: f32, rng: &mut impl rand::Rng) {
17        self.for_each(|x| x.mutate(ctx, rate, rng));
18    }
19}
20
21/// Used in dividually-reproducing [`Repopulator`]s
22pub trait Mitosis: Clone {
23    /// Simulation-wide context required for this mitosis implementation.
24    type Context;
25
26    /// Create a new child with mutation. Similar to [`RandomlyMutable::mutate`], but returns a new instance instead of modifying the original.
27    fn divide(&self, ctx: &<Self as Mitosis>::Context, rate: f32, rng: &mut impl rand::Rng)
28        -> Self;
29}
30
31impl<T: Mitosis> Mitosis for Vec<T> {
32    type Context = T::Context;
33
34    fn divide(
35        &self,
36        ctx: &<Self as Mitosis>::Context,
37        rate: f32,
38        rng: &mut impl rand::Rng,
39    ) -> Self {
40        let mut child = Vec::with_capacity(self.len());
41        for gene in self {
42            child.push(gene.divide(ctx, rate, rng));
43        }
44        child
45    }
46}
47
48/// Repopulator that uses division reproduction to create new genomes.
49pub struct MitosisRepopulator<G: Mitosis> {
50    /// The mutation rate to use when mutating genomes. 0.0 - 1.0
51    pub mutation_rate: f32,
52
53    /// The context to use when mutating genomes.
54    pub ctx: G::Context,
55    _marker: std::marker::PhantomData<G>,
56}
57
58impl<G: Mitosis> MitosisRepopulator<G> {
59    /// Creates a new [`MitosisRepopulator`].
60    pub fn new(mutation_rate: f32, ctx: G::Context) -> Self {
61        Self {
62            mutation_rate,
63            ctx,
64            _marker: std::marker::PhantomData,
65        }
66    }
67}
68
69impl<G> Repopulator<G> for MitosisRepopulator<G>
70where
71    G: Mitosis,
72{
73    fn repopulate(&mut self, genomes: &mut Vec<G>, target_size: usize) {
74        let mut rng = rand::rng();
75        let champions = genomes.clone();
76        let mut champs_cycle = champions.iter().cycle();
77
78        // TODO maybe rayonify
79        while genomes.len() < target_size {
80            let parent = champs_cycle.next().unwrap();
81            let child = parent.divide(&self.ctx, self.mutation_rate, &mut rng);
82            genomes.push(child);
83        }
84    }
85}
86
87#[cfg(feature = "crossover")]
88mod crossover {
89    use rand::RngExt;
90
91    use super::*;
92
93    /// Used in crossover-reproducing [`Repopulator`]s
94    pub trait Crossover: Clone {
95        /// Simulation-wide context required for this crossover implementation.
96        type Context;
97
98        /// Use crossover reproduction to create a new genome.
99        fn crossover(
100            &self,
101            other: &Self,
102            ctx: &Self::Context,
103            rate: f32,
104            rng: &mut impl rand::Rng,
105        ) -> Self;
106    }
107
108    /// Repopulator that uses crossover reproduction to create new genomes.
109    pub struct CrossoverRepopulator<G: Crossover> {
110        /// The mutation rate to use when mutating genomes. 0.0 - 1.0
111        pub mutation_rate: f32,
112
113        /// Additional context for crossover/mutation.
114        pub ctx: G::Context,
115        _marker: std::marker::PhantomData<G>,
116    }
117
118    impl<G: Crossover> CrossoverRepopulator<G> {
119        /// Creates a new [`CrossoverRepopulator`].
120        pub fn new(mutation_rate: f32, ctx: G::Context) -> Self {
121            Self {
122                mutation_rate,
123                ctx,
124                _marker: std::marker::PhantomData,
125            }
126        }
127    }
128
129    impl<G> Repopulator<G> for CrossoverRepopulator<G>
130    where
131        G: Crossover,
132    {
133        fn repopulate(&mut self, genomes: &mut Vec<G>, target_size: usize) {
134            let mut rng = rand::rng();
135            let champions = genomes.clone();
136            let mut champs_cycle = champions.iter().enumerate().cycle();
137
138            // TODO maybe rayonify
139            while genomes.len() < target_size {
140                let (i, parent1) = champs_cycle.next().unwrap();
141                let mut j = rng.random_range(1..champions.len());
142                if i == j {
143                    j = 0;
144                }
145                let parent2 = &genomes[j];
146
147                let child = parent1.crossover(parent2, &self.ctx, self.mutation_rate, &mut rng);
148
149                genomes.push(child);
150            }
151        }
152    }
153}
154
155#[cfg(feature = "crossover")]
156pub use crossover::*;
157
158#[cfg(feature = "speciation")]
159mod speciation {
160    use std::collections::HashMap;
161
162    use rand::RngExt;
163
164    use super::*;
165
166    /// Used in speciated crossover nextgens. Allows for genomes to avoid crossover with ones that are too different.
167    pub trait Speciated {
168        /// The type used to distinguish
169        /// one genome's species from another.
170        type Species: Eq + std::hash::Hash; // I really don't like that we need `Eq` when `PartialEq` better fits the definiton.
171
172        /// Get/calculate this genome's species.
173        fn species(&self) -> Self::Species;
174    }
175
176    /// Repopulator that uses crossover reproduction to create new genomes, but only between genomes of the same species.
177    pub struct SpeciatedCrossoverRepopulator<G: Crossover + Speciated> {
178        /// The inner crossover repopulator. This holds the settings for crossover operations,
179        /// but may also be called if [`allow_emergency_repr`][Self::allow_emergency_repr] is `true`.
180        pub crossover: CrossoverRepopulator<G>,
181
182        /// Whether to allow genomes to reproduce across species boundaries
183        /// (effectively vanilla crossover)
184        /// in emergency situations where no genomes have compatible partners.
185        /// If disabled, the simulation will panic in such a situation.
186        pub allow_emergency_repr: bool,
187
188        _marker: std::marker::PhantomData<G>,
189    }
190
191    impl<G: Crossover + Speciated> SpeciatedCrossoverRepopulator<G> {
192        /// Creates a new [`SpeciatedCrossoverRepopulator`].
193        pub fn new(mutation_rate: f32, allow_emergency_repr: bool, ctx: G::Context) -> Self {
194            Self {
195                crossover: CrossoverRepopulator::new(mutation_rate, ctx),
196                allow_emergency_repr,
197                _marker: std::marker::PhantomData,
198            }
199        }
200    }
201
202    impl<G> Repopulator<G> for SpeciatedCrossoverRepopulator<G>
203    where
204        G: Crossover + Speciated,
205    {
206        // i'm still not really satisfied with this implementation,
207        // but it's better than the old one.
208        fn repopulate(&mut self, genomes: &mut Vec<G>, target_size: usize) {
209            let initial_size = genomes.len();
210            let mut rng = rand::rng();
211            let mut species: HashMap<<G as Speciated>::Species, Vec<&G>> = HashMap::new();
212
213            for genome in genomes.iter() {
214                let spec = genome.species();
215                species.entry(spec).or_insert_with(Vec::new).push(genome);
216            }
217
218            let mut species_iter = species.values();
219            let to_create = target_size - initial_size;
220            let mut new_genomes = Vec::with_capacity(to_create);
221
222            while new_genomes.len() < to_create {
223                if let Some(spec) = species_iter.next() {
224                    if spec.len() < 2 {
225                        continue;
226                    }
227
228                    for (i, &parent1) in spec.iter().enumerate() {
229                        let mut j = rng.random_range(1..spec.len());
230                        if j == i {
231                            j = 0;
232                        }
233                        let parent2 = spec[j];
234
235                        new_genomes.push(parent1.crossover(
236                            parent2,
237                            &self.crossover.ctx,
238                            self.crossover.mutation_rate,
239                            &mut rng,
240                        ));
241                    }
242                } else {
243                    // reached the end, reset the iterator
244
245                    if new_genomes.is_empty() {
246                        // no genomes have compatible partners
247                        if self.allow_emergency_repr {
248                            self.crossover.repopulate(genomes, target_size);
249                            return;
250                        } else {
251                            panic!("no genomes with common species");
252                        }
253                    }
254
255                    species_iter = species.values();
256                }
257            }
258
259            genomes.extend(new_genomes);
260        }
261    }
262}
263
264#[cfg(feature = "speciation")]
265pub use speciation::*;