1use rand::seq::SliceRandom;
6use rand::Rng;
7use rand_distr::{Distribution, WeightedIndex};
8
9use crate::genome::traits::EvolutionaryGenome;
10use crate::operators::traits::SelectionOperator;
11
12#[derive(Clone, Debug)]
35pub struct TournamentSelection {
36 pub tournament_size: usize,
38 pub with_replacement: bool,
41}
42
43impl TournamentSelection {
44 pub fn new(tournament_size: usize) -> Self {
47 assert!(tournament_size >= 1, "Tournament size must be at least 1");
48 Self {
49 tournament_size,
50 with_replacement: true,
51 }
52 }
53
54 pub fn binary() -> Self {
56 Self::new(2)
57 }
58
59 pub fn without_replacement(tournament_size: usize) -> Self {
65 assert!(tournament_size >= 1, "Tournament size must be at least 1");
66 Self {
67 tournament_size,
68 with_replacement: false,
69 }
70 }
71}
72
73impl<G: EvolutionaryGenome> SelectionOperator<G> for TournamentSelection {
74 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
75 assert!(!population.is_empty(), "Population cannot be empty");
76
77 let n = population.len();
78
79 let tournament: Vec<usize> = if self.with_replacement {
80 (0..self.tournament_size)
82 .map(|_| rng.gen_range(0..n))
83 .collect()
84 } else {
85 let k = self.tournament_size.min(n);
87 (0..n)
88 .collect::<Vec<usize>>()
89 .choose_multiple(rng, k)
90 .copied()
91 .collect()
92 };
93
94 tournament
96 .into_iter()
97 .max_by(|&a, &b| {
98 population[a]
99 .1
100 .partial_cmp(&population[b].1)
101 .unwrap_or(std::cmp::Ordering::Equal)
102 })
103 .unwrap()
104 }
105}
106
107#[derive(Clone, Debug)]
111pub struct RouletteSelection {
112 offset: f64,
114}
115
116impl RouletteSelection {
117 pub fn new() -> Self {
119 Self { offset: 0.0 }
120 }
121
122 pub fn with_offset(offset: f64) -> Self {
124 Self { offset }
125 }
126}
127
128impl Default for RouletteSelection {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl<G: EvolutionaryGenome> SelectionOperator<G> for RouletteSelection {
135 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
136 assert!(!population.is_empty(), "Population cannot be empty");
137
138 let min_fitness = population
140 .iter()
141 .map(|(_, f)| *f)
142 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
143 .unwrap();
144
145 let offset = if min_fitness < 0.0 {
146 -min_fitness + self.offset + 1.0
147 } else {
148 self.offset
149 };
150
151 let weights: Vec<f64> = population.iter().map(|(_, f)| f + offset).collect();
153
154 let total: f64 = weights.iter().sum();
156 if total <= 0.0 {
157 return rng.gen_range(0..population.len());
158 }
159
160 match WeightedIndex::new(&weights) {
162 Ok(dist) => dist.sample(rng),
163 Err(_) => rng.gen_range(0..population.len()),
164 }
165 }
166}
167
168#[derive(Clone, Debug)]
172pub struct TruncationSelection {
173 pub truncation_ratio: f64,
175}
176
177impl TruncationSelection {
178 pub fn new(truncation_ratio: f64) -> Self {
180 assert!(
181 truncation_ratio > 0.0 && truncation_ratio <= 1.0,
182 "Truncation ratio must be in (0, 1]"
183 );
184 Self { truncation_ratio }
185 }
186}
187
188impl<G: EvolutionaryGenome> SelectionOperator<G> for TruncationSelection {
189 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
190 assert!(!population.is_empty(), "Population cannot be empty");
191
192 let mut indices: Vec<usize> = (0..population.len()).collect();
194 indices.sort_by(|&a, &b| {
195 population[b]
196 .1
197 .partial_cmp(&population[a].1)
198 .unwrap_or(std::cmp::Ordering::Equal)
199 });
200
201 let cutoff = ((population.len() as f64) * self.truncation_ratio).ceil() as usize;
203 let cutoff = cutoff.max(1);
204
205 indices[rng.gen_range(0..cutoff)]
206 }
207}
208
209#[derive(Clone, Debug)]
213pub struct RankSelection {
214 pub selection_pressure: f64,
216}
217
218impl RankSelection {
219 pub fn new(selection_pressure: f64) -> Self {
221 assert!(
222 (1.0..=2.0).contains(&selection_pressure),
223 "Selection pressure must be in [1.0, 2.0]"
224 );
225 Self { selection_pressure }
226 }
227}
228
229impl Default for RankSelection {
230 fn default() -> Self {
231 Self::new(1.5)
232 }
233}
234
235impl<G: EvolutionaryGenome> SelectionOperator<G> for RankSelection {
236 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
237 assert!(!population.is_empty(), "Population cannot be empty");
238
239 let n = population.len();
240 let sp = self.selection_pressure;
241
242 let mut indices: Vec<usize> = (0..n).collect();
244 indices.sort_by(|&a, &b| {
245 population[a]
246 .1
247 .partial_cmp(&population[b].1)
248 .unwrap_or(std::cmp::Ordering::Equal)
249 });
250
251 let weights: Vec<f64> = (0..n)
254 .map(|rank| {
255 if n == 1 {
256 1.0
257 } else {
258 2.0 - sp + 2.0 * (sp - 1.0) * (rank as f64) / ((n - 1) as f64)
259 }
260 })
261 .collect();
262
263 match WeightedIndex::new(&weights) {
264 Ok(dist) => indices[dist.sample(rng)],
265 Err(_) => indices[rng.gen_range(0..n)],
266 }
267 }
268}
269
270#[derive(Clone, Debug)]
274pub struct BoltzmannSelection {
275 pub temperature: f64,
277}
278
279impl BoltzmannSelection {
280 pub fn new(temperature: f64) -> Self {
282 assert!(temperature > 0.0, "Temperature must be positive");
283 Self { temperature }
284 }
285}
286
287impl<G: EvolutionaryGenome> SelectionOperator<G> for BoltzmannSelection {
288 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
289 assert!(!population.is_empty(), "Population cannot be empty");
290
291 let scaled: Vec<f64> = population
293 .iter()
294 .map(|(_, f)| f / self.temperature)
295 .collect();
296 let max_scaled = scaled.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
297
298 let weights: Vec<f64> = scaled.iter().map(|s| (s - max_scaled).exp()).collect();
299
300 match WeightedIndex::new(&weights) {
301 Ok(dist) => dist.sample(rng),
302 Err(_) => rng.gen_range(0..population.len()),
303 }
304 }
305}
306
307#[derive(Clone, Debug, Default)]
309pub struct RandomSelection;
310
311impl RandomSelection {
312 pub fn new() -> Self {
314 Self
315 }
316}
317
318impl<G: EvolutionaryGenome> SelectionOperator<G> for RandomSelection {
319 fn select<R: Rng>(&self, population: &[(G, f64)], rng: &mut R) -> usize {
320 assert!(!population.is_empty(), "Population cannot be empty");
321 rng.gen_range(0..population.len())
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::genome::real_vector::RealVector;
329
330 fn create_population(size: usize) -> Vec<(RealVector, f64)> {
331 (0..size)
332 .map(|i| (RealVector::new(vec![i as f64]), i as f64))
333 .collect()
334 }
335
336 #[test]
337 fn test_tournament_selection_selects_valid_index() {
338 let mut rng = rand::thread_rng();
339 let population = create_population(10);
340 let selection = TournamentSelection::new(3);
341
342 for _ in 0..100 {
343 let idx = selection.select(&population, &mut rng);
344 assert!(idx < population.len());
345 }
346 }
347
348 #[test]
349 fn test_tournament_selection_binary() {
350 let selection = TournamentSelection::binary();
351 assert_eq!(selection.tournament_size, 2);
352 }
353
354 #[test]
355 fn test_tournament_selection_prefers_fitter() {
356 let mut rng = rand::thread_rng();
357 let population: Vec<(RealVector, f64)> = vec![
359 (RealVector::new(vec![0.0]), 0.0),
360 (RealVector::new(vec![1.0]), 100.0), (RealVector::new(vec![2.0]), 0.0),
362 ];
363
364 let selection = TournamentSelection::without_replacement(3);
367
368 let mut best_count = 0;
369 let trials = 100;
370 for _ in 0..trials {
371 let idx = selection.select(&population, &mut rng);
372 if idx == 1 {
373 best_count += 1;
374 }
375 }
376
377 assert_eq!(best_count, trials);
379 }
380
381 #[test]
382 fn test_tournament_with_replacement_is_not_deterministic_at_full_size() {
383 use rand::SeedableRng;
391 let mut rng = rand::rngs::StdRng::seed_from_u64(7);
392
393 let population: Vec<(RealVector, f64)> = (0..5)
395 .map(|i| (RealVector::new(vec![i as f64]), i as f64))
396 .collect();
397
398 let selection = TournamentSelection::new(5); assert!(selection.with_replacement);
400
401 let trials = 300;
402 let mut best_count = 0;
403 for _ in 0..trials {
404 if selection.select(&population, &mut rng) == 4 {
405 best_count += 1;
406 }
407 }
408
409 assert!(
413 best_count < trials,
414 "with-replacement tournament should not be deterministic at k == n (got {best_count}/{trials})"
415 );
416 assert!(
418 best_count > trials / 2,
419 "expected the fittest to win most tournaments (got {best_count}/{trials})"
420 );
421 }
422
423 #[test]
424 fn test_tournament_without_replacement_full_size_is_elitist() {
425 let mut rng = rand::thread_rng();
428 let population: Vec<(RealVector, f64)> = (0..5)
429 .map(|i| (RealVector::new(vec![i as f64]), i as f64))
430 .collect();
431
432 let selection = TournamentSelection::without_replacement(5);
433 for _ in 0..50 {
434 assert_eq!(selection.select(&population, &mut rng), 4);
435 }
436 }
437
438 #[test]
439 fn test_roulette_selection_selects_valid_index() {
440 let mut rng = rand::thread_rng();
441 let population = create_population(10);
442 let selection = RouletteSelection::new();
443
444 for _ in 0..100 {
445 let idx = selection.select(&population, &mut rng);
446 assert!(idx < population.len());
447 }
448 }
449
450 #[test]
451 fn test_roulette_selection_handles_negative_fitness() {
452 let mut rng = rand::thread_rng();
453 let population: Vec<(RealVector, f64)> = vec![
454 (RealVector::new(vec![0.0]), -10.0),
455 (RealVector::new(vec![1.0]), -5.0),
456 (RealVector::new(vec![2.0]), -1.0),
457 ];
458
459 let selection = RouletteSelection::new();
460
461 for _ in 0..100 {
462 let idx = selection.select(&population, &mut rng);
463 assert!(idx < population.len());
464 }
465 }
466
467 #[test]
468 fn test_truncation_selection_selects_from_top() {
469 let mut rng = rand::thread_rng();
470 let population = create_population(10);
471 let selection = TruncationSelection::new(0.2); for _ in 0..100 {
474 let idx = selection.select(&population, &mut rng);
475 assert!(idx >= 8);
477 }
478 }
479
480 #[test]
481 fn test_rank_selection_selects_valid_index() {
482 let mut rng = rand::thread_rng();
483 let population = create_population(10);
484 let selection = RankSelection::new(1.5);
485
486 for _ in 0..100 {
487 let idx = selection.select(&population, &mut rng);
488 assert!(idx < population.len());
489 }
490 }
491
492 #[test]
493 fn test_boltzmann_selection_selects_valid_index() {
494 let mut rng = rand::thread_rng();
495 let population = create_population(10);
496 let selection = BoltzmannSelection::new(1.0);
497
498 for _ in 0..100 {
499 let idx = selection.select(&population, &mut rng);
500 assert!(idx < population.len());
501 }
502 }
503
504 #[test]
505 fn test_boltzmann_selection_temperature_effect() {
506 let mut rng = rand::thread_rng();
507 let population: Vec<(RealVector, f64)> = vec![
509 (RealVector::new(vec![0.0]), 0.0),
510 (RealVector::new(vec![1.0]), 10.0),
511 ];
512
513 let low_temp = BoltzmannSelection::new(0.1);
515 let high_temp = BoltzmannSelection::new(100.0);
517
518 let mut low_best_count = 0;
519 let mut high_best_count = 0;
520 let trials = 1000;
521
522 for _ in 0..trials {
523 if low_temp.select(&population, &mut rng) == 1 {
524 low_best_count += 1;
525 }
526 if high_temp.select(&population, &mut rng) == 1 {
527 high_best_count += 1;
528 }
529 }
530
531 assert!(low_best_count > high_best_count);
533 }
534
535 #[test]
536 fn test_random_selection_uniform() {
537 let mut rng = rand::thread_rng();
538 let population = create_population(2);
539 let selection = RandomSelection::new();
540
541 let mut counts = [0, 0];
542 let trials = 1000;
543
544 for _ in 0..trials {
545 counts[selection.select(&population, &mut rng)] += 1;
546 }
547
548 let ratio = counts[0] as f64 / counts[1] as f64;
550 assert!(ratio > 0.8 && ratio < 1.2);
551 }
552
553 #[test]
554 fn test_select_many() {
555 let mut rng = rand::thread_rng();
556 let population = create_population(10);
557 let selection = TournamentSelection::new(3);
558
559 let indices = selection.select_many(&population, 5, &mut rng);
560 assert_eq!(indices.len(), 5);
561 for idx in indices {
562 assert!(idx < population.len());
563 }
564 }
565
566 #[test]
567 #[should_panic(expected = "Tournament size must be at least 1")]
568 fn test_tournament_size_zero() {
569 TournamentSelection::new(0);
570 }
571
572 #[test]
573 #[should_panic(expected = "Truncation ratio must be in (0, 1]")]
574 fn test_truncation_ratio_zero() {
575 TruncationSelection::new(0.0);
576 }
577
578 #[test]
579 #[should_panic(expected = "Temperature must be positive")]
580 fn test_boltzmann_temperature_zero() {
581 BoltzmannSelection::new(0.0);
582 }
583}