Skip to main content

radiate_engines/steps/
recombine.rs

1use crate::steps::EngineStep;
2use radiate_core::error::RadiateResult;
3use radiate_core::{
4    Alterer, Chromosome, Ecosystem, MetricSet, Objective, Optimize, Population, Score, Select,
5};
6use radiate_core::{Phenotype, radiate_err};
7use radiate_error::Result;
8use radiate_utils::VersionedCounts;
9use std::sync::Arc;
10
11#[derive(Clone)]
12pub struct SelectConfig<C: Chromosome> {
13    pub(crate) count: usize,
14    pub(crate) selector: Arc<dyn Select<C>>,
15    pub(crate) names: (&'static str, &'static str),
16}
17
18impl<C: Chromosome> SelectConfig<C> {
19    pub fn new(
20        count: usize,
21        selector: Arc<dyn Select<C>>,
22        names: (&'static str, &'static str),
23    ) -> Self {
24        Self {
25            count,
26            selector,
27            names,
28        }
29    }
30}
31
32#[derive(Clone)]
33pub struct SurvivorConfig<C: Chromosome> {
34    pub(crate) select: SelectConfig<C>,
35}
36
37impl<C: Chromosome> SurvivorConfig<C> {
38    pub fn new(select: SelectConfig<C>) -> Self {
39        Self { select }
40    }
41}
42
43#[derive(Clone)]
44pub struct OffspringConfig<C: Chromosome> {
45    pub(crate) select: SelectConfig<C>,
46    pub(crate) alters: Vec<Alterer<C>>,
47}
48
49impl<C: Chromosome> OffspringConfig<C> {
50    pub fn new(select: SelectConfig<C>, alters: Vec<Alterer<C>>) -> Self {
51        Self { select, alters }
52    }
53}
54
55pub struct RecombineStep<C: Chromosome> {
56    pub(crate) survivor: SurvivorConfig<C>,
57    pub(crate) offspring: OffspringConfig<C>,
58    pub(crate) objective: Objective,
59    pub(crate) survivor_counts: VersionedCounts,
60    pub(crate) offspring_counts: VersionedCounts,
61}
62
63impl<C: Chromosome> RecombineStep<C> {
64    pub fn new(
65        survivor: SurvivorConfig<C>,
66        offspring: OffspringConfig<C>,
67        objective: Objective,
68    ) -> Self {
69        Self {
70            survivor,
71            offspring,
72            objective,
73            survivor_counts: VersionedCounts::new(),
74            offspring_counts: VersionedCounts::new(),
75        }
76    }
77}
78
79impl<C> EngineStep<C> for RecombineStep<C>
80where
81    C: Chromosome + PartialEq + Clone,
82{
83    #[inline]
84    fn execute(
85        &mut self,
86        generation: usize,
87        ecosystem: &mut Ecosystem<C>,
88        metrics: &mut MetricSet,
89    ) -> Result<()> {
90        let new_members = if ecosystem.species().is_some() {
91            self.recombine_clustered(generation, ecosystem, metrics)
92        } else {
93            self.recombine_flat(generation, ecosystem, metrics)
94        };
95
96        match new_members {
97            Ok((survivors, offspring)) => {
98                let pop = ecosystem.population_mut();
99
100                pop.clear();
101                pop.extend(survivors);
102                pop.extend(offspring);
103
104                Ok(())
105            }
106            Err(err) => Err(err.context("Recombination Step failed")),
107        }
108    }
109}
110
111impl<C> RecombineStep<C>
112where
113    C: Chromosome + PartialEq + Clone,
114{
115    /// Non-species path: one descending walk that builds both survivors and
116    /// offspring from the population. Each unique source idx yields exactly
117    /// one `swap_remove` move regardless of how many survivor or offspring
118    /// slots it fills, so we save a clone per idx that appears in both
119    /// selections (or in survivors only). On the other hand, some indices that
120    /// fill only one bucket can be moved over from previous generation to
121    /// current without cloning. In practice this can save ~20-50% of clones compared to
122    /// a naive separate-walk approach.
123    #[inline]
124    fn recombine_flat(
125        &mut self,
126        generation: usize,
127        ecosystem: &mut Ecosystem<C>,
128        metrics: &mut MetricSet,
129    ) -> RadiateResult<(Population<C>, Population<C>)> {
130        let s_selector = &self.survivor.select;
131        let o_selector = &self.offspring.select;
132
133        let pop_slice = ecosystem.population().as_ref();
134        let pop_len = pop_slice.len();
135
136        let s_indices = self.timed_select(s_selector, pop_slice, metrics);
137        let o_indices = self.timed_select(o_selector, pop_slice, metrics);
138
139        self.offspring_counts.begin(pop_len);
140        for &idx in o_indices.iter() {
141            self.offspring_counts.bump(idx);
142        }
143
144        self.survivor_counts.begin(pop_len);
145        for &idx in s_indices.iter() {
146            self.survivor_counts.bump(idx);
147        }
148
149        let (survivors, mut offspring) = self.unioned_walk(ecosystem);
150
151        self.objective.sort(&mut offspring);
152
153        for alt in &mut self.offspring.alters {
154            alt.alter(offspring.as_mut(), metrics, generation)?;
155        }
156
157        Ok((survivors, offspring))
158    }
159
160    /// Species path: per-species reproduction. Survivors are selected globally
161    /// via the survivor selector, then per-species offspring quotas drive
162    /// scoped selection + alteration within each species' sub-pop. This follows
163    /// a pretty similar approach to the above method, but we split the logic up by
164    /// species, so each species essentially performs the above algorithm in it's own search space.
165    #[inline]
166    fn recombine_clustered(
167        &mut self,
168        generation: usize,
169        ecosystem: &mut Ecosystem<C>,
170        metrics: &mut MetricSet,
171    ) -> RadiateResult<(Population<C>, Population<C>)> {
172        let s_selector = &self.survivor.select;
173        let o_selector = &self.offspring.select;
174
175        let (species, population) = ecosystem.species_population_mut();
176        let species = species.as_ref().ok_or(
177            radiate_err!(Step: self.name(), "Species population is None during recombination"),
178        )?;
179
180        population.sort_by(|a, b| a.species().cmp(&b.species()));
181
182        let mut start = 0;
183        let mut species_groups = Vec::with_capacity(species.len());
184        let slice = population.as_ref();
185        for chunk in slice.chunk_by(|a, b| a.species() == b.species()) {
186            species_groups.push((chunk[0].species(), start..start + chunk.len()));
187            start += chunk.len();
188        }
189
190        let mut species_scores = species
191            .iter()
192            .filter_map(|spec| spec.adj_score())
193            .collect::<Vec<_>>();
194
195        if let Objective::Single(Optimize::Minimize) = &self.objective {
196            species_scores.reverse();
197        }
198
199        let quotas = self.quotas_from_scores(&species_scores);
200
201        self.offspring_counts.begin(population.len());
202        for (species, count) in species.iter().zip(quotas.iter()) {
203            let range = species_groups
204                .binary_search_by(|group| group.0.cmp(&species.id()))
205                .ok()
206                .map(|i| species_groups[i].1.clone())
207                .ok_or(radiate_err!(Step: self.name(), "Failed to find species {:?} in population", species.id()))?;
208
209            let mut sub_pop = &mut population[range.clone()];
210            self.objective.sort(&mut sub_pop);
211
212            let offspring = self.timed_select_count(o_selector, sub_pop, metrics, *count);
213
214            for &idx in offspring.iter() {
215                self.offspring_counts.bump(range.start + idx);
216            }
217        }
218
219        let s_indices = self.timed_select(s_selector, population.as_ref(), metrics);
220
221        self.survivor_counts.begin(population.len());
222        for &idx in s_indices.iter() {
223            self.survivor_counts.bump(idx);
224        }
225
226        let (survivors, mut offspring) = self.unioned_walk(ecosystem);
227
228        let o_slice = offspring.as_mut();
229        for sub_pop in o_slice.chunk_by_mut(|a, b| a.species() == b.species()) {
230            let mut chunk = sub_pop;
231            self.objective.sort(&mut chunk);
232
233            for alt in &mut self.offspring.alters {
234                alt.alter(chunk, metrics, generation)?;
235            }
236        }
237
238        Ok((survivors, offspring))
239    }
240
241    #[inline]
242    fn timed_select(
243        &self,
244        select: &SelectConfig<C>,
245        population: &[Phenotype<C>],
246        metrics: &mut MetricSet,
247    ) -> Vec<usize> {
248        self.timed_select_count(select, population, metrics, select.count)
249    }
250
251    #[inline]
252    fn timed_select_count(
253        &self,
254        select: &SelectConfig<C>,
255        population: &[Phenotype<C>],
256        metrics: &mut MetricSet,
257        count: usize,
258    ) -> Vec<usize> {
259        let timer = std::time::Instant::now();
260        let indices = select.selector.select(population, &self.objective, count);
261        metrics.upsert(select.names.0, indices.len());
262        metrics.upsert(select.names.1, timer.elapsed());
263        indices
264    }
265
266    /// So, I was pulling my hair out over this for a bit because I knew it was possible but
267    /// couldn't quite get it right. However, now that we've arrived at an elegant solution, this
268    /// approach is pretty significant. The key insight is that we can interleave the survivor and offspring
269    /// creation in a single walk over the union of selected indices, which allows us to save a clone for each
270    /// index that appears in both selections. In practice this can save
271    /// ~20-50% of clones compared to a naive approach.
272    ///
273    /// In other words:
274    /// Single descending walk over the union of selected indices.
275    /// For each unique source idx with total = s (survivors) + o (offspring) > 0, emit (total - 1)
276    /// clones distributed to whichever bucket still needs entries, then
277    /// swap_remove the last one and place it in whichever bucket has room.
278    ///
279    /// So total emissions = (total - 1) + 1 = total.
280    /// Suppose s = 3, o = 2 for some idx (so total = 5):
281    ///
282    /// loop iteration 1: s_left=3 > 0, clone -> survivors[0], s_left=2
283    /// loop iteration 2: s_left=2 > 0, clone -> survivors[1], s_left=1
284    /// loop iteration 3: s_left=1 > 0, clone -> survivors[2], s_left=0
285    /// loop iteration 4: s_left=0,     clone -> offspring[0]
286    ///
287    /// --- loop ends after total - 1 = 4 iterations ---
288    ///
289    /// swap_remove:      s_left=0,     move  -> offspring[1]
290    ///
291    /// Result: 3 clones to survivors, 1 clone & 1 move to offspring.
292    /// This results in 4 clones total instead of 5. One deep Phenotype<C> clone saved
293    /// per unique source idx.
294    #[inline]
295    fn unioned_walk(&self, ecosystem: &mut Ecosystem<C>) -> (Population<C>, Population<C>) {
296        let mut survivors = Population::with_capacity(self.survivor.select.count);
297        let mut offspring = Population::with_capacity(self.offspring.select.count);
298
299        let pop = ecosystem.population_mut();
300        let iter = self
301            .survivor_counts
302            .iter_pair_live_rev(&self.offspring_counts);
303
304        for (idx, s, o) in iter {
305            let mut s_left = s;
306            let total = s_left + o;
307
308            for _ in 0..total - 1 {
309                if s_left > 0 {
310                    survivors.push(pop[idx].clone());
311                    s_left -= 1;
312                } else {
313                    offspring.push(pop[idx].clone());
314                }
315            }
316
317            let moved = pop.swap_remove(idx);
318            if s_left > 0 {
319                survivors.push(moved);
320            } else {
321                offspring.push(moved);
322            }
323        }
324
325        (survivors, offspring)
326    }
327
328    #[inline]
329    fn quotas_from_scores(&self, scores: &[&Score]) -> Vec<usize> {
330        let n = scores.len();
331        if n == 0 || self.offspring.select.count == 0 {
332            return vec![0; n];
333        }
334
335        let raw_scores = scores.iter().map(|s| s.as_f32()).collect::<Vec<f32>>();
336        let mut min_score = raw_scores.iter().cloned().fold(f32::INFINITY, f32::min);
337        if !min_score.is_finite() {
338            min_score = 0.0;
339        }
340
341        let shifted = raw_scores
342            .iter()
343            .map(|s| (s - min_score).max(0.0))
344            .collect::<Vec<f32>>();
345
346        let sum = shifted.iter().sum::<f32>();
347
348        if sum <= f32::EPSILON {
349            let base = self.offspring.select.count / n;
350            let mut quotas = vec![base; n];
351            let mut remaining = self.offspring.select.count - base * n;
352            let mut i = 0;
353            while remaining > 0 {
354                quotas[i] += 1;
355                remaining -= 1;
356                i += 1;
357            }
358
359            return quotas;
360        }
361
362        let total = self.offspring.select.count as f32;
363
364        let mut quotas = Vec::with_capacity(n);
365        let mut fracs = Vec::with_capacity(n);
366        let mut assigned = 0;
367
368        for (idx, w) in shifted.iter().enumerate() {
369            let p = *w / sum;
370            let exact = p * total;
371            let base = exact.floor() as usize;
372            let frac = exact - base as f32;
373
374            quotas.push(base);
375            fracs.push((frac, idx));
376            assigned += base;
377        }
378
379        let remaining = self.offspring.select.count.saturating_sub(assigned);
380        fracs.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
381
382        for (_, idx) in fracs.iter().take(remaining) {
383            quotas[*idx] += 1;
384        }
385
386        quotas
387    }
388}