Skip to main content

fugue_evo/population/
population.rs

1//! Population type
2//!
3//! This module provides the Population container type.
4
5use rand::Rng;
6
7#[cfg(feature = "parallel")]
8use rayon::prelude::*;
9
10use crate::fitness::traits::{Fitness, FitnessValue};
11use crate::genome::bounds::MultiBounds;
12use crate::genome::traits::EvolutionaryGenome;
13use crate::population::individual::Individual;
14
15/// A population of individuals
16#[derive(Clone, Debug)]
17pub struct Population<G, F = f64>
18where
19    G: EvolutionaryGenome,
20    F: FitnessValue,
21{
22    /// The individuals in this population
23    individuals: Vec<Individual<G, F>>,
24    /// Current generation number
25    generation: usize,
26}
27
28impl<G, F> Population<G, F>
29where
30    G: EvolutionaryGenome,
31    F: FitnessValue,
32{
33    /// Create an empty population
34    pub fn new() -> Self {
35        Self {
36            individuals: Vec::new(),
37            generation: 0,
38        }
39    }
40
41    /// Create a population with the given capacity
42    pub fn with_capacity(capacity: usize) -> Self {
43        Self {
44            individuals: Vec::with_capacity(capacity),
45            generation: 0,
46        }
47    }
48
49    /// Create a population from a vector of individuals
50    pub fn from_individuals(individuals: Vec<Individual<G, F>>) -> Self {
51        Self {
52            individuals,
53            generation: 0,
54        }
55    }
56
57    /// Create a random population
58    pub fn random<R: Rng>(size: usize, bounds: &MultiBounds, rng: &mut R) -> Self {
59        let individuals = (0..size)
60            .map(|_| Individual::new(G::generate(rng, bounds)))
61            .collect();
62        Self {
63            individuals,
64            generation: 0,
65        }
66    }
67
68    /// Get the current generation
69    pub fn generation(&self) -> usize {
70        self.generation
71    }
72
73    /// Increment the generation counter
74    pub fn increment_generation(&mut self) {
75        self.generation += 1;
76    }
77
78    /// Set the generation number
79    pub fn set_generation(&mut self, generation: usize) {
80        self.generation = generation;
81    }
82
83    /// Get the population size
84    pub fn len(&self) -> usize {
85        self.individuals.len()
86    }
87
88    /// Check if the population is empty
89    pub fn is_empty(&self) -> bool {
90        self.individuals.is_empty()
91    }
92
93    /// Get an individual by index
94    pub fn get(&self, index: usize) -> Option<&Individual<G, F>> {
95        self.individuals.get(index)
96    }
97
98    /// Get a mutable reference to an individual by index
99    pub fn get_mut(&mut self, index: usize) -> Option<&mut Individual<G, F>> {
100        self.individuals.get_mut(index)
101    }
102
103    /// Add an individual to the population
104    pub fn push(&mut self, individual: Individual<G, F>) {
105        self.individuals.push(individual);
106    }
107
108    /// Remove and return the last individual
109    pub fn pop(&mut self) -> Option<Individual<G, F>> {
110        self.individuals.pop()
111    }
112
113    /// Clear the population
114    pub fn clear(&mut self) {
115        self.individuals.clear();
116    }
117
118    /// Get an iterator over the individuals
119    pub fn iter(&self) -> impl Iterator<Item = &Individual<G, F>> {
120        self.individuals.iter()
121    }
122
123    /// Get a mutable iterator over the individuals
124    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Individual<G, F>> {
125        self.individuals.iter_mut()
126    }
127
128    /// Get the underlying vector of individuals
129    pub fn individuals(&self) -> &[Individual<G, F>] {
130        &self.individuals
131    }
132
133    /// Get mutable access to the underlying vector
134    pub fn individuals_mut(&mut self) -> &mut Vec<Individual<G, F>> {
135        &mut self.individuals
136    }
137
138    /// Take the individuals out of this population
139    pub fn into_individuals(self) -> Vec<Individual<G, F>> {
140        self.individuals
141    }
142
143    /// Get the best individual (by fitness)
144    ///
145    /// Ranks by [`FitnessValue::cmp_by_quality`] (which delegates to
146    /// `is_better_than` and treats any `NaN` fitness as strictly worst), so it
147    /// returns the true best even for fitness types whose `to_f64()` ordering
148    /// disagrees with the real ordering (e.g. `ParetoFitness` with infinite
149    /// crowding distances) and never returns a `NaN`-fitness individual unless
150    /// every evaluated individual is `NaN`.
151    pub fn best(&self) -> Option<&Individual<G, F>> {
152        self.individuals
153            .iter()
154            .filter(|i| i.is_evaluated())
155            .max_by(|a, b| {
156                a.fitness
157                    .as_ref()
158                    .expect("filtered to evaluated individuals")
159                    .cmp_by_quality(
160                        b.fitness
161                            .as_ref()
162                            .expect("filtered to evaluated individuals"),
163                    )
164            })
165    }
166
167    /// Get the worst individual (by fitness)
168    ///
169    /// Uses the same [`FitnessValue::cmp_by_quality`] total order as
170    /// [`best`](Self::best); a `NaN` fitness is ranked strictly worst.
171    pub fn worst(&self) -> Option<&Individual<G, F>> {
172        self.individuals
173            .iter()
174            .filter(|i| i.is_evaluated())
175            .min_by(|a, b| {
176                a.fitness
177                    .as_ref()
178                    .expect("filtered to evaluated individuals")
179                    .cmp_by_quality(
180                        b.fitness
181                            .as_ref()
182                            .expect("filtered to evaluated individuals"),
183                    )
184            })
185    }
186
187    /// Sort the population by fitness (best first)
188    ///
189    /// Ranks by [`FitnessValue::cmp_by_quality`] rather than a `to_f64()`
190    /// scalar. Unevaluated individuals (and, defensively, `NaN` fitness) sort
191    /// last.
192    pub fn sort_by_fitness(&mut self) {
193        self.individuals.sort_by(|a, b| {
194            match (a.fitness.as_ref(), b.fitness.as_ref()) {
195                // Better first => descending by quality.
196                (Some(fa), Some(fb)) => fb.cmp_by_quality(fa),
197                // Evaluated individuals precede unevaluated ones.
198                (Some(_), None) => std::cmp::Ordering::Less,
199                (None, Some(_)) => std::cmp::Ordering::Greater,
200                (None, None) => std::cmp::Ordering::Equal,
201            }
202        });
203    }
204
205    /// Truncate the population to the given size, keeping the best individuals
206    pub fn truncate_to_best(&mut self, size: usize) {
207        self.sort_by_fitness();
208        self.individuals.truncate(size);
209    }
210
211    /// Check if all individuals have been evaluated
212    pub fn all_evaluated(&self) -> bool {
213        self.individuals.iter().all(|i| i.is_evaluated())
214    }
215
216    /// Count the number of evaluated individuals
217    pub fn count_evaluated(&self) -> usize {
218        self.individuals.iter().filter(|i| i.is_evaluated()).count()
219    }
220
221    /// Get genome-fitness pairs for selection
222    pub fn as_selection_pool(&self) -> Vec<(&G, f64)> {
223        self.individuals
224            .iter()
225            .filter_map(|i| i.fitness.as_ref().map(|f| (&i.genome, f.to_f64())))
226            .collect()
227    }
228
229    /// Get genome-fitness pairs as owned tuples
230    pub fn as_fitness_pairs(&self) -> Vec<(G, f64)>
231    where
232        G: Clone,
233    {
234        self.individuals
235            .iter()
236            .filter_map(|i| i.fitness.as_ref().map(|f| (i.genome.clone(), f.to_f64())))
237            .collect()
238    }
239
240    /// Evaluate all individuals using the given fitness function (sequential)
241    pub fn evaluate<Fit>(&mut self, fitness: &Fit)
242    where
243        Fit: Fitness<Genome = G, Value = F>,
244    {
245        for individual in &mut self.individuals {
246            if !individual.is_evaluated() {
247                let f = fitness.evaluate(&individual.genome);
248                individual.set_fitness(f);
249            }
250        }
251    }
252
253    /// Compute mean fitness
254    pub fn mean_fitness(&self) -> Option<f64> {
255        let evaluated: Vec<f64> = self
256            .individuals
257            .iter()
258            .filter_map(|i| i.fitness.as_ref().map(|f| f.to_f64()))
259            .collect();
260
261        if evaluated.is_empty() {
262            None
263        } else {
264            Some(evaluated.iter().sum::<f64>() / evaluated.len() as f64)
265        }
266    }
267
268    /// Compute fitness standard deviation
269    pub fn fitness_std(&self) -> Option<f64> {
270        let mean = self.mean_fitness()?;
271        let evaluated: Vec<f64> = self
272            .individuals
273            .iter()
274            .filter_map(|i| i.fitness.as_ref().map(|f| f.to_f64()))
275            .collect();
276
277        if evaluated.len() < 2 {
278            return None;
279        }
280
281        let variance = evaluated.iter().map(|f| (f - mean).powi(2)).sum::<f64>()
282            / (evaluated.len() - 1) as f64;
283        Some(variance.sqrt())
284    }
285
286    /// Compute population diversity (average pairwise distance)
287    pub fn diversity(&self) -> f64 {
288        if self.len() < 2 {
289            return 0.0;
290        }
291
292        let mut total_distance = 0.0;
293        let mut count = 0;
294
295        for i in 0..self.len() {
296            for j in (i + 1)..self.len() {
297                total_distance += self.individuals[i]
298                    .genome
299                    .distance(&self.individuals[j].genome);
300                count += 1;
301            }
302        }
303
304        if count == 0 {
305            0.0
306        } else {
307            total_distance / count as f64
308        }
309    }
310}
311
312/// Parallel evaluation support (requires `parallel` feature)
313#[cfg(feature = "parallel")]
314impl<G, F> Population<G, F>
315where
316    G: EvolutionaryGenome + Send + Sync,
317    F: FitnessValue + Send,
318{
319    /// Evaluate all individuals using the given fitness function (parallel)
320    pub fn evaluate_parallel<Fit>(&mut self, fitness: &Fit)
321    where
322        Fit: Fitness<Genome = G, Value = F> + Sync,
323    {
324        self.individuals
325            .par_iter_mut()
326            .filter(|i| !i.is_evaluated())
327            .for_each(|individual| {
328                let f = fitness.evaluate(&individual.genome);
329                individual.set_fitness(f);
330            });
331    }
332}
333
334/// Sequential fallback for parallel evaluation (when `parallel` feature is disabled)
335#[cfg(not(feature = "parallel"))]
336impl<G, F> Population<G, F>
337where
338    G: EvolutionaryGenome,
339    F: FitnessValue,
340{
341    /// Evaluate all individuals using the given fitness function (sequential fallback)
342    ///
343    /// Note: This is a sequential implementation used when the `parallel` feature is disabled.
344    pub fn evaluate_parallel<Fit>(&mut self, fitness: &Fit)
345    where
346        Fit: Fitness<Genome = G, Value = F>,
347    {
348        self.evaluate(fitness);
349    }
350}
351
352impl<G, F> Default for Population<G, F>
353where
354    G: EvolutionaryGenome,
355    F: FitnessValue,
356{
357    fn default() -> Self {
358        Self::new()
359    }
360}
361
362impl<G, F> std::ops::Index<usize> for Population<G, F>
363where
364    G: EvolutionaryGenome,
365    F: FitnessValue,
366{
367    type Output = Individual<G, F>;
368
369    fn index(&self, index: usize) -> &Self::Output {
370        &self.individuals[index]
371    }
372}
373
374impl<G, F> std::ops::IndexMut<usize> for Population<G, F>
375where
376    G: EvolutionaryGenome,
377    F: FitnessValue,
378{
379    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
380        &mut self.individuals[index]
381    }
382}
383
384impl<G, F> IntoIterator for Population<G, F>
385where
386    G: EvolutionaryGenome,
387    F: FitnessValue,
388{
389    type Item = Individual<G, F>;
390    type IntoIter = std::vec::IntoIter<Individual<G, F>>;
391
392    fn into_iter(self) -> Self::IntoIter {
393        self.individuals.into_iter()
394    }
395}
396
397impl<G, F> FromIterator<Individual<G, F>> for Population<G, F>
398where
399    G: EvolutionaryGenome,
400    F: FitnessValue,
401{
402    fn from_iter<I: IntoIterator<Item = Individual<G, F>>>(iter: I) -> Self {
403        Self::from_individuals(iter.into_iter().collect())
404    }
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410    use crate::fitness::benchmarks::Sphere;
411    use crate::fitness::traits::ParetoFitness;
412    use crate::genome::real_vector::RealVector;
413
414    fn create_test_population() -> Population<RealVector> {
415        let individuals = vec![
416            Individual::with_fitness(RealVector::new(vec![1.0]), 10.0),
417            Individual::with_fitness(RealVector::new(vec![2.0]), 20.0),
418            Individual::with_fitness(RealVector::new(vec![3.0]), 30.0),
419            Individual::with_fitness(RealVector::new(vec![4.0]), 40.0),
420            Individual::with_fitness(RealVector::new(vec![5.0]), 50.0),
421        ];
422        Population::from_individuals(individuals)
423    }
424
425    #[test]
426    fn test_population_new() {
427        let pop: Population<RealVector> = Population::new();
428        assert!(pop.is_empty());
429        assert_eq!(pop.generation(), 0);
430    }
431
432    #[test]
433    fn test_population_random() {
434        let mut rng = rand::thread_rng();
435        let bounds = MultiBounds::symmetric(5.0, 3);
436        let pop: Population<RealVector> = Population::random(10, &bounds, &mut rng);
437
438        assert_eq!(pop.len(), 10);
439        assert!(!pop.all_evaluated());
440    }
441
442    #[test]
443    fn test_population_best_worst() {
444        let pop = create_test_population();
445
446        let best = pop.best().unwrap();
447        assert_eq!(best.fitness_f64(), 50.0);
448
449        let worst = pop.worst().unwrap();
450        assert_eq!(worst.fitness_f64(), 10.0);
451    }
452
453    #[test]
454    fn test_best_ignores_nan_fitness() {
455        // regression: EV-07
456        // A NaN fitness must never be reported as the best individual. It is
457        // ranked strictly worst, so best() returns the true maximum even when
458        // the NaN individual is the LAST element -- the position that used to
459        // fool max_by (NaN comparisons returned Equal, treated as "replace").
460        let mut individuals = vec![
461            Individual::with_fitness(RealVector::new(vec![1.0]), 10.0),
462            Individual::with_fitness(RealVector::new(vec![3.0]), 30.0),
463            Individual::with_fitness(RealVector::new(vec![2.0]), 20.0),
464        ];
465        // Inject NaN through the public field to model a value slipping past
466        // set_fitness's guard (defense in depth), placed last.
467        let mut nan_ind = Individual::new(RealVector::new(vec![9.0]));
468        nan_ind.fitness = Some(f64::NAN);
469        individuals.push(nan_ind);
470
471        let pop = Population::from_individuals(individuals);
472        assert_eq!(pop.best().unwrap().fitness_f64(), 30.0);
473        // worst() over finite values (NaN is ranked worst and would be picked,
474        // but here we confirm the true minimum among the reals is reachable by
475        // dropping the NaN): with the NaN present it is the worst.
476        assert!(pop.worst().unwrap().fitness_f64().is_nan());
477    }
478
479    #[test]
480    fn test_best_worst_sort_use_is_better_than_for_pareto() {
481        // regression: EV-08
482        // ParetoFitness ranks by (rank asc, crowding desc). NSGA-II assigns
483        // f64::INFINITY crowding to front-boundary members, so to_f64() =
484        // -(rank) + inf*0.001 collapses to +inf for EVERY rank. A to_f64()-based
485        // best() therefore cannot distinguish a rank-0 optimum from a rank-50
486        // solution; best()/worst()/sort_by_fitness() must delegate to
487        // is_better_than().
488        let mut good = ParetoFitness::new(vec![0.0, 0.0]);
489        good.rank = 0;
490        good.crowding_distance = f64::INFINITY;
491
492        let mut bad = ParetoFitness::new(vec![9.0, 9.0]);
493        bad.rank = 50;
494        bad.crowding_distance = f64::INFINITY;
495
496        assert_eq!(good.to_f64(), bad.to_f64()); // both +inf: to_f64() cannot rank them
497
498        // `bad` placed last so a to_f64()/Equal tie would wrongly "take later".
499        let individuals = vec![
500            Individual::with_fitness(RealVector::new(vec![0.0]), good.clone()),
501            Individual::with_fitness(RealVector::new(vec![1.0]), bad.clone()),
502        ];
503        let mut pop: Population<RealVector, ParetoFitness> =
504            Population::from_individuals(individuals);
505
506        assert_eq!(pop.best().unwrap().fitness.as_ref().unwrap().rank, 0);
507        assert_eq!(pop.worst().unwrap().fitness.as_ref().unwrap().rank, 50);
508
509        pop.sort_by_fitness();
510        assert_eq!(pop[0].fitness.as_ref().unwrap().rank, 0); // best first
511        assert_eq!(pop[1].fitness.as_ref().unwrap().rank, 50);
512    }
513
514    #[test]
515    fn test_population_sort_by_fitness() {
516        let mut pop = create_test_population();
517        pop.sort_by_fitness();
518
519        let fitnesses: Vec<f64> = pop.iter().map(|i| i.fitness_f64()).collect();
520        assert_eq!(fitnesses, vec![50.0, 40.0, 30.0, 20.0, 10.0]);
521    }
522
523    #[test]
524    fn test_population_truncate_to_best() {
525        let mut pop = create_test_population();
526        pop.truncate_to_best(3);
527
528        assert_eq!(pop.len(), 3);
529        let fitnesses: Vec<f64> = pop.iter().map(|i| i.fitness_f64()).collect();
530        assert_eq!(fitnesses, vec![50.0, 40.0, 30.0]);
531    }
532
533    #[test]
534    fn test_population_mean_fitness() {
535        let pop = create_test_population();
536        let mean = pop.mean_fitness().unwrap();
537        assert_eq!(mean, 30.0); // (10 + 20 + 30 + 40 + 50) / 5
538    }
539
540    #[test]
541    fn test_population_fitness_std() {
542        let pop = create_test_population();
543        let std = pop.fitness_std().unwrap();
544        // Variance = ((10-30)^2 + (20-30)^2 + (30-30)^2 + (40-30)^2 + (50-30)^2) / 4
545        // = (400 + 100 + 0 + 100 + 400) / 4 = 250
546        // Std = sqrt(250) ≈ 15.81
547        assert!((std - 15.81).abs() < 0.1);
548    }
549
550    #[test]
551    fn test_population_evaluate() {
552        let mut rng = rand::thread_rng();
553        let bounds = MultiBounds::symmetric(5.0, 3);
554        let mut pop: Population<RealVector> = Population::random(5, &bounds, &mut rng);
555
556        let fitness = Sphere::new(3);
557        pop.evaluate(&fitness);
558
559        assert!(pop.all_evaluated());
560        assert_eq!(pop.count_evaluated(), 5);
561    }
562
563    #[test]
564    fn test_population_evaluate_parallel() {
565        let mut rng = rand::thread_rng();
566        let bounds = MultiBounds::symmetric(5.0, 3);
567        let mut pop: Population<RealVector> = Population::random(100, &bounds, &mut rng);
568
569        let fitness = Sphere::new(3);
570        pop.evaluate_parallel(&fitness);
571
572        assert!(pop.all_evaluated());
573        assert_eq!(pop.count_evaluated(), 100);
574    }
575
576    #[test]
577    fn test_population_generation() {
578        let mut pop = create_test_population();
579        assert_eq!(pop.generation(), 0);
580
581        pop.increment_generation();
582        assert_eq!(pop.generation(), 1);
583
584        pop.set_generation(100);
585        assert_eq!(pop.generation(), 100);
586    }
587
588    #[test]
589    fn test_population_push_pop() {
590        let mut pop: Population<RealVector> = Population::new();
591
592        pop.push(Individual::with_fitness(RealVector::new(vec![1.0]), 10.0));
593        assert_eq!(pop.len(), 1);
594
595        let ind = pop.pop().unwrap();
596        assert_eq!(ind.fitness_f64(), 10.0);
597        assert!(pop.is_empty());
598    }
599
600    #[test]
601    fn test_population_indexing() {
602        let pop = create_test_population();
603        assert_eq!(pop[0].fitness_f64(), 10.0);
604        assert_eq!(pop[4].fitness_f64(), 50.0);
605    }
606
607    #[test]
608    fn test_population_as_selection_pool() {
609        let pop = create_test_population();
610        let pool = pop.as_selection_pool();
611
612        assert_eq!(pool.len(), 5);
613        assert_eq!(pool[0].1, 10.0);
614        assert_eq!(pool[4].1, 50.0);
615    }
616
617    #[test]
618    fn test_population_diversity() {
619        let individuals = vec![
620            Individual::with_fitness(RealVector::new(vec![0.0, 0.0]), 1.0),
621            Individual::with_fitness(RealVector::new(vec![1.0, 0.0]), 1.0),
622            Individual::with_fitness(RealVector::new(vec![0.0, 1.0]), 1.0),
623        ];
624        let pop = Population::from_individuals(individuals);
625
626        let diversity = pop.diversity();
627        // Average of distances: (1, 1, sqrt(2)) / 3 ≈ 1.14
628        assert!(diversity > 1.0 && diversity < 1.2);
629    }
630
631    #[test]
632    fn test_population_from_iterator() {
633        let individuals = vec![
634            Individual::with_fitness(RealVector::new(vec![1.0]), 10.0),
635            Individual::with_fitness(RealVector::new(vec![2.0]), 20.0),
636        ];
637        let pop: Population<RealVector> = individuals.into_iter().collect();
638
639        assert_eq!(pop.len(), 2);
640    }
641
642    #[test]
643    fn test_population_into_iterator() {
644        let pop = create_test_population();
645        let individuals: Vec<_> = pop.into_iter().collect();
646
647        assert_eq!(individuals.len(), 5);
648    }
649}