use rand::seq::SliceRandom;
use rand::Rng;
use rand_distr::{Distribution, WeightedIndex};
use crate::genome::traits::EvolutionaryGenome;
use crate::operators::traits::SelectionOperator;
#[derive(Clone, Debug)]
pub struct TournamentSelection {
pub tournament_size: usize,
pub with_replacement: bool,
}
impl TournamentSelection {
pub fn new(tournament_size: usize) -> Self {
assert!(tournament_size >= 1, "Tournament size must be at least 1");
Self {
tournament_size,
with_replacement: true,
}
}
pub fn binary() -> Self {
Self::new(2)
}
pub fn without_replacement(tournament_size: usize) -> Self {
assert!(tournament_size >= 1, "Tournament size must be at least 1");
Self {
tournament_size,
with_replacement: false,
}
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for TournamentSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
let n = population.len();
let tournament: Vec<usize> = if self.with_replacement {
(0..self.tournament_size)
.map(|_| rng.gen_range(0..n))
.collect()
} else {
let k = self.tournament_size.min(n);
(0..n)
.collect::<Vec<usize>>()
.choose_multiple(rng, k)
.copied()
.collect()
};
tournament
.into_iter()
.max_by(|&a, &b| {
population[a]
.1
.partial_cmp(&population[b].1)
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap()
}
}
#[derive(Clone, Debug)]
pub struct RouletteSelection {
offset: f64,
}
impl RouletteSelection {
pub fn new() -> Self {
Self { offset: 0.0 }
}
pub fn with_offset(offset: f64) -> Self {
Self { offset }
}
}
impl Default for RouletteSelection {
fn default() -> Self {
Self::new()
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for RouletteSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
let min_fitness = population
.iter()
.map(|(_, f)| *f)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
let offset = if min_fitness < 0.0 {
-min_fitness + self.offset + 1.0
} else {
self.offset
};
let weights: Vec<f64> = population.iter().map(|(_, f)| f + offset).collect();
let total: f64 = weights.iter().sum();
if total <= 0.0 {
return rng.gen_range(0..population.len());
}
match WeightedIndex::new(&weights) {
Ok(dist) => dist.sample(rng),
Err(_) => rng.gen_range(0..population.len()),
}
}
}
#[derive(Clone, Debug)]
pub struct TruncationSelection {
pub truncation_ratio: f64,
}
impl TruncationSelection {
pub fn new(truncation_ratio: f64) -> Self {
assert!(
truncation_ratio > 0.0 && truncation_ratio <= 1.0,
"Truncation ratio must be in (0, 1]"
);
Self { truncation_ratio }
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for TruncationSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
let mut indices: Vec<usize> = (0..population.len()).collect();
indices.sort_by(|&a, &b| {
population[b]
.1
.partial_cmp(&population[a].1)
.unwrap_or(std::cmp::Ordering::Equal)
});
let cutoff = ((population.len() as f64) * self.truncation_ratio).ceil() as usize;
let cutoff = cutoff.max(1);
indices[rng.gen_range(0..cutoff)]
}
}
#[derive(Clone, Debug)]
pub struct RankSelection {
pub selection_pressure: f64,
}
impl RankSelection {
pub fn new(selection_pressure: f64) -> Self {
assert!(
(1.0..=2.0).contains(&selection_pressure),
"Selection pressure must be in [1.0, 2.0]"
);
Self { selection_pressure }
}
}
impl Default for RankSelection {
fn default() -> Self {
Self::new(1.5)
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for RankSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
let n = population.len();
let sp = self.selection_pressure;
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
population[a]
.1
.partial_cmp(&population[b].1)
.unwrap_or(std::cmp::Ordering::Equal)
});
let weights: Vec<f64> = (0..n)
.map(|rank| {
if n == 1 {
1.0
} else {
2.0 - sp + 2.0 * (sp - 1.0) * (rank as f64) / ((n - 1) as f64)
}
})
.collect();
match WeightedIndex::new(&weights) {
Ok(dist) => indices[dist.sample(rng)],
Err(_) => indices[rng.gen_range(0..n)],
}
}
}
#[derive(Clone, Debug)]
pub struct BoltzmannSelection {
pub temperature: f64,
}
impl BoltzmannSelection {
pub fn new(temperature: f64) -> Self {
assert!(temperature > 0.0, "Temperature must be positive");
Self { temperature }
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for BoltzmannSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
let scaled: Vec<f64> = population
.iter()
.map(|(_, f)| f / self.temperature)
.collect();
let max_scaled = scaled.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let weights: Vec<f64> = scaled.iter().map(|s| (s - max_scaled).exp()).collect();
match WeightedIndex::new(&weights) {
Ok(dist) => dist.sample(rng),
Err(_) => rng.gen_range(0..population.len()),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct RandomSelection;
impl RandomSelection {
pub fn new() -> Self {
Self
}
}
impl<G: EvolutionaryGenome> SelectionOperator<G> for RandomSelection {
fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
assert!(!population.is_empty(), "Population cannot be empty");
rng.gen_range(0..population.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genome::real_vector::RealVector;
fn create_population(size: usize) -> Vec<(RealVector, f64)> {
(0..size)
.map(|i| (RealVector::new(vec![i as f64]), i as f64))
.collect()
}
#[test]
fn test_tournament_selection_selects_valid_index() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = TournamentSelection::new(3);
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx < population.len());
}
}
#[test]
fn test_tournament_selection_binary() {
let selection = TournamentSelection::binary();
assert_eq!(selection.tournament_size, 2);
}
#[test]
fn test_tournament_selection_prefers_fitter() {
let mut rng = rand::thread_rng();
let population: Vec<(RealVector, f64)> = vec![
(RealVector::new(vec![0.0]), 0.0),
(RealVector::new(vec![1.0]), 100.0), (RealVector::new(vec![2.0]), 0.0),
];
let selection = TournamentSelection::without_replacement(3);
let mut best_count = 0;
let trials = 100;
for _ in 0..trials {
let idx = selection.select(&population, &mut rng);
if idx == 1 {
best_count += 1;
}
}
assert_eq!(best_count, trials);
}
#[test]
fn test_tournament_with_replacement_is_not_deterministic_at_full_size() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
let population: Vec<(RealVector, f64)> = (0..5)
.map(|i| (RealVector::new(vec![i as f64]), i as f64))
.collect();
let selection = TournamentSelection::new(5); assert!(selection.with_replacement);
let trials = 300;
let mut best_count = 0;
for _ in 0..trials {
if selection.select(&population, &mut rng) == 4 {
best_count += 1;
}
}
assert!(
best_count < trials,
"with-replacement tournament should not be deterministic at k == n (got {best_count}/{trials})"
);
assert!(
best_count > trials / 2,
"expected the fittest to win most tournaments (got {best_count}/{trials})"
);
}
#[test]
fn test_tournament_without_replacement_full_size_is_elitist() {
let mut rng = rand::thread_rng();
let population: Vec<(RealVector, f64)> = (0..5)
.map(|i| (RealVector::new(vec![i as f64]), i as f64))
.collect();
let selection = TournamentSelection::without_replacement(5);
for _ in 0..50 {
assert_eq!(selection.select(&population, &mut rng), 4);
}
}
#[test]
fn test_roulette_selection_selects_valid_index() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = RouletteSelection::new();
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx < population.len());
}
}
#[test]
fn test_roulette_selection_handles_negative_fitness() {
let mut rng = rand::thread_rng();
let population: Vec<(RealVector, f64)> = vec![
(RealVector::new(vec![0.0]), -10.0),
(RealVector::new(vec![1.0]), -5.0),
(RealVector::new(vec![2.0]), -1.0),
];
let selection = RouletteSelection::new();
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx < population.len());
}
}
#[test]
fn test_truncation_selection_selects_from_top() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = TruncationSelection::new(0.2);
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx >= 8);
}
}
#[test]
fn test_rank_selection_selects_valid_index() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = RankSelection::new(1.5);
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx < population.len());
}
}
#[test]
fn test_boltzmann_selection_selects_valid_index() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = BoltzmannSelection::new(1.0);
for _ in 0..100 {
let idx = selection.select(&population, &mut rng);
assert!(idx < population.len());
}
}
#[test]
fn test_boltzmann_selection_temperature_effect() {
let mut rng = rand::thread_rng();
let population: Vec<(RealVector, f64)> = vec![
(RealVector::new(vec![0.0]), 0.0),
(RealVector::new(vec![1.0]), 10.0),
];
let low_temp = BoltzmannSelection::new(0.1);
let high_temp = BoltzmannSelection::new(100.0);
let mut low_best_count = 0;
let mut high_best_count = 0;
let trials = 1000;
for _ in 0..trials {
if low_temp.select(&population, &mut rng) == 1 {
low_best_count += 1;
}
if high_temp.select(&population, &mut rng) == 1 {
high_best_count += 1;
}
}
assert!(low_best_count > high_best_count);
}
#[test]
fn test_random_selection_uniform() {
let mut rng = rand::thread_rng();
let population = create_population(2);
let selection = RandomSelection::new();
let mut counts = [0, 0];
let trials = 1000;
for _ in 0..trials {
counts[selection.select(&population, &mut rng)] += 1;
}
let ratio = counts[0] as f64 / counts[1] as f64;
assert!(ratio > 0.8 && ratio < 1.2);
}
#[test]
fn test_select_many() {
let mut rng = rand::thread_rng();
let population = create_population(10);
let selection = TournamentSelection::new(3);
let indices = selection.select_many(&population, 5, &mut rng);
assert_eq!(indices.len(), 5);
for idx in indices {
assert!(idx < population.len());
}
}
#[test]
#[should_panic(expected = "Tournament size must be at least 1")]
fn test_tournament_size_zero() {
TournamentSelection::new(0);
}
#[test]
#[should_panic(expected = "Truncation ratio must be in (0, 1]")]
fn test_truncation_ratio_zero() {
TruncationSelection::new(0.0);
}
#[test]
#[should_panic(expected = "Temperature must be positive")]
fn test_boltzmann_temperature_zero() {
BoltzmannSelection::new(0.0);
}
}