rlevo_evolution/ops/
selection.rs1use burn::tensor::{backend::Backend, Int, Tensor, TensorData};
15use rand::{Rng, RngExt};
16
17#[must_use]
27pub fn tournament_indices_host(
28 fitness: &[f32],
29 tournament_size: usize,
30 n_winners: usize,
31 rng: &mut dyn Rng,
32) -> Vec<i64> {
33 assert!(!fitness.is_empty(), "fitness must be non-empty");
34 assert!(tournament_size >= 2, "tournament size must be >= 2");
35 let pop_size = fitness.len();
36 let mut winners = Vec::with_capacity(n_winners);
37 for _ in 0..n_winners {
38 let mut best_idx = rng.random_range(0..pop_size);
39 let mut best_f = fitness[best_idx];
40 for _ in 1..tournament_size {
41 let idx = rng.random_range(0..pop_size);
42 if fitness[idx] < best_f {
43 best_f = fitness[idx];
44 best_idx = idx;
45 }
46 }
47 #[allow(clippy::cast_possible_wrap)]
48 winners.push(best_idx as i64);
49 }
50 winners
51}
52
53#[must_use]
63pub fn tournament_select<B: Backend>(
64 population: &Tensor<B, 2>,
65 fitness: &[f32],
66 tournament_size: usize,
67 n_winners: usize,
68 rng: &mut dyn Rng,
69 device: &B::Device,
70) -> Tensor<B, 2> {
71 let winners = tournament_indices_host(fitness, tournament_size, n_winners, rng);
72 let indices = Tensor::<B, 1, Int>::from_data(TensorData::new(winners, [n_winners]), device);
73 population.clone().select(0, indices)
74}
75
76#[must_use]
83pub fn truncation_indices_host(fitness: &[f32], top_k: usize) -> Vec<i64> {
84 assert!(!fitness.is_empty(), "fitness must be non-empty");
85 assert!(top_k <= fitness.len(), "top_k must be <= population size");
86 let mut indexed: Vec<(usize, f32)> = fitness.iter().copied().enumerate().collect();
87 indexed
88 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
89 #[allow(clippy::cast_possible_wrap)]
90 indexed
91 .into_iter()
92 .take(top_k)
93 .map(|(i, _)| i as i64)
94 .collect()
95}
96
97#[must_use]
104pub fn truncation_select<B: Backend>(
105 population: &Tensor<B, 2>,
106 fitness: &[f32],
107 top_k: usize,
108 device: &B::Device,
109) -> Tensor<B, 2> {
110 let winners = truncation_indices_host(fitness, top_k);
111 let indices = Tensor::<B, 1, Int>::from_data(TensorData::new(winners, [top_k]), device);
112 population.clone().select(0, indices)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use burn::backend::NdArray;
119 use rand::SeedableRng;
120 use rand::rngs::StdRng;
121
122 type TestBackend = NdArray;
123
124 #[test]
125 fn tournament_prefers_better_fitness_in_expectation() {
126 let mut rng = StdRng::seed_from_u64(1);
127 let fitness = [100.0f32, 0.0, 100.0, 100.0];
128 let winners = tournament_indices_host(&fitness, 2, 1000, &mut rng);
129 let wins_for_best = winners.iter().filter(|&&w| w == 1).count();
130 assert!(
134 (350..=550).contains(&wins_for_best),
135 "wins_for_best={wins_for_best} (expected ~437)",
136 );
137 }
138
139 #[test]
140 fn truncation_returns_smallest_fitness_first() {
141 let fitness = [5.0f32, 1.0, 3.0, 2.0, 4.0];
142 let idx = truncation_indices_host(&fitness, 3);
143 assert_eq!(idx.len(), 3);
145 assert!(idx.contains(&1));
146 assert!(idx.contains(&3));
147 assert!(idx.contains(&2));
148 }
149
150 #[test]
151 fn tournament_select_returns_shaped_tensor() {
152 let device = Default::default();
153 let data = TensorData::new(vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0], [3, 2]);
154 let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
155 let fitness = [10.0_f32, 0.0, 10.0];
156 let mut rng = StdRng::seed_from_u64(2);
157 let parents = tournament_select(&pop, &fitness, 2, 4, &mut rng, &device);
158 assert_eq!(parents.shape().dims, vec![4, 2]);
159 }
160}