use burn::tensor::{backend::Backend, Int, Tensor, TensorData};
use rand::{Rng, RngExt};
#[must_use]
pub fn tournament_indices_host(
fitness: &[f32],
tournament_size: usize,
n_winners: usize,
rng: &mut dyn Rng,
) -> Vec<i64> {
assert!(!fitness.is_empty(), "fitness must be non-empty");
assert!(tournament_size >= 2, "tournament size must be >= 2");
let pop_size = fitness.len();
let mut winners = Vec::with_capacity(n_winners);
for _ in 0..n_winners {
let mut best_idx = rng.random_range(0..pop_size);
let mut best_f = fitness[best_idx];
for _ in 1..tournament_size {
let idx = rng.random_range(0..pop_size);
if fitness[idx] < best_f {
best_f = fitness[idx];
best_idx = idx;
}
}
#[allow(clippy::cast_possible_wrap)]
winners.push(best_idx as i64);
}
winners
}
#[must_use]
pub fn tournament_select<B: Backend>(
population: &Tensor<B, 2>,
fitness: &[f32],
tournament_size: usize,
n_winners: usize,
rng: &mut dyn Rng,
device: &B::Device,
) -> Tensor<B, 2> {
let winners = tournament_indices_host(fitness, tournament_size, n_winners, rng);
let indices = Tensor::<B, 1, Int>::from_data(TensorData::new(winners, [n_winners]), device);
population.clone().select(0, indices)
}
#[must_use]
pub fn truncation_indices_host(fitness: &[f32], top_k: usize) -> Vec<i64> {
assert!(!fitness.is_empty(), "fitness must be non-empty");
assert!(top_k <= fitness.len(), "top_k must be <= population size");
let mut indexed: Vec<(usize, f32)> = fitness.iter().copied().enumerate().collect();
indexed
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
#[allow(clippy::cast_possible_wrap)]
indexed
.into_iter()
.take(top_k)
.map(|(i, _)| i as i64)
.collect()
}
#[must_use]
pub fn truncation_select<B: Backend>(
population: &Tensor<B, 2>,
fitness: &[f32],
top_k: usize,
device: &B::Device,
) -> Tensor<B, 2> {
let winners = truncation_indices_host(fitness, top_k);
let indices = Tensor::<B, 1, Int>::from_data(TensorData::new(winners, [top_k]), device);
population.clone().select(0, indices)
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::NdArray;
use rand::SeedableRng;
use rand::rngs::StdRng;
type TestBackend = NdArray;
#[test]
fn tournament_prefers_better_fitness_in_expectation() {
let mut rng = StdRng::seed_from_u64(1);
let fitness = [100.0f32, 0.0, 100.0, 100.0];
let winners = tournament_indices_host(&fitness, 2, 1000, &mut rng);
let wins_for_best = winners.iter().filter(|&&w| w == 1).count();
assert!(
(350..=550).contains(&wins_for_best),
"wins_for_best={wins_for_best} (expected ~437)",
);
}
#[test]
fn truncation_returns_smallest_fitness_first() {
let fitness = [5.0f32, 1.0, 3.0, 2.0, 4.0];
let idx = truncation_indices_host(&fitness, 3);
assert_eq!(idx.len(), 3);
assert!(idx.contains(&1));
assert!(idx.contains(&3));
assert!(idx.contains(&2));
}
#[test]
fn tournament_select_returns_shaped_tensor() {
let device = Default::default();
let data = TensorData::new(vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0], [3, 2]);
let pop = Tensor::<TestBackend, 2>::from_data(data, &device);
let fitness = [10.0_f32, 0.0, 10.0];
let mut rng = StdRng::seed_from_u64(2);
let parents = tournament_select(&pop, &fitness, 2, 4, &mut rng, &device);
assert_eq!(parents.shape().dims, vec![4, 2]);
}
}