use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
use rand::{Rng, RngExt};
#[must_use]
pub fn argmax_host(fitness: &[f32]) -> usize {
assert!(!fitness.is_empty(), "fitness must be non-empty");
let mut best_idx = 0usize;
let mut best = f32::NEG_INFINITY;
for (i, &v) in fitness.iter().enumerate() {
if v > best {
best = v;
best_idx = i;
}
}
best_idx
}
#[must_use]
pub fn tournament_indices_host(
fitness: &[f32],
tournament_size: usize,
n_winners: usize,
rng: &mut dyn Rng,
) -> Vec<i32> {
assert!(!fitness.is_empty(), "fitness must be non-empty");
assert!(tournament_size >= 1, "tournament size must be >= 1");
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, clippy::cast_possible_truncation)]
winners.push(best_idx as i32);
}
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 as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 2> {
assert_eq!(
population.dims()[0],
fitness.len(),
"tournament_select: population rows ({}) must equal fitness.len() ({})",
population.dims()[0],
fitness.len(),
);
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<i32> {
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()
.map(|&f| crate::fitness::sanitize_fitness(f))
.enumerate()
.collect();
indexed.sort_by(|a, b| b.1.total_cmp(&a.1));
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
indexed
.into_iter()
.take(top_k)
.map(|(i, _)| i as i32)
.collect()
}
#[must_use]
pub fn truncation_select<B: Backend>(
population: &Tensor<B, 2>,
fitness: &[f32],
top_k: usize,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 2> {
assert_eq!(
population.dims()[0],
fitness.len(),
"truncation_select: population rows ({}) must equal fitness.len() ({})",
population.dims()[0],
fitness.len(),
);
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::Flex;
use rand::SeedableRng;
use rand::rngs::StdRng;
type TestBackend = Flex;
#[test]
fn tournament_prefers_better_fitness_in_expectation() {
let mut rng = StdRng::seed_from_u64(1);
let fitness = [0.0f32, 100.0, 0.0, 0.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_largest_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(&0));
assert!(idx.contains(&4));
assert!(idx.contains(&2));
}
#[test]
fn argmax_returns_index_of_largest() {
assert_eq!(argmax_host(&[1.0, 5.0, 3.0]), 1);
assert_eq!(argmax_host(&[-3.0, -1.0, -2.0]), 1);
assert_eq!(argmax_host(&[7.0]), 0);
}
#[test]
fn argmax_tie_resolves_to_lowest_index() {
assert_eq!(argmax_host(&[2.0, 5.0, 5.0, 5.0]), 1);
}
#[test]
fn argmax_nan_never_wins() {
assert_eq!(argmax_host(&[1.0, f32::NAN, 3.0]), 2);
assert_eq!(argmax_host(&[f32::NAN, 1.0]), 1);
}
#[test]
fn argmax_degenerate_slice_falls_back_to_zero() {
assert_eq!(argmax_host(&[f32::NAN, f32::NAN]), 0);
assert_eq!(argmax_host(&[f32::NAN, f32::NEG_INFINITY]), 0);
assert_eq!(argmax_host(&[f32::NEG_INFINITY, f32::NEG_INFINITY]), 0);
}
#[test]
#[should_panic(expected = "fitness must be non-empty")]
fn argmax_empty_panics() {
let _ = argmax_host(&[]);
}
#[test]
fn tournament_size_one_is_uniform_random() {
let mut rng = StdRng::seed_from_u64(3);
let fitness = [0.0f32, 100.0, 0.0, 0.0];
let winners = tournament_indices_host(&fitness, 1, 1000, &mut rng);
let wins_for_best = winners.iter().filter(|&&w| w == 1).count();
assert!(
(150..=350).contains(&wins_for_best),
"wins_for_best={wins_for_best} (expected ~250)",
);
}
#[test]
fn truncation_sorts_nan_fitness_last() {
let fitness = [3.0f32, f32::NAN, 5.0, 1.0];
let idx = truncation_indices_host(&fitness, fitness.len());
assert_eq!(idx.len(), 4);
assert_eq!(&idx[..3], &[2, 0, 3]);
assert_eq!(idx[3], 1);
}
#[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 = [0.0_f32, 10.0, 0.0];
let mut rng = StdRng::seed_from_u64(2);
let parents = tournament_select(&pop, &fitness, 2, 4, &mut rng, &device);
assert_eq!(parents.dims(), [4, 2]);
}
}