Skip to main content

fugue_evo/population/
individual.rs

1//! Individual wrapper type
2//!
3//! This module provides the Individual type that wraps a genome with its fitness.
4
5use std::cmp::Ordering;
6
7use serde::{Deserialize, Serialize};
8
9use crate::fitness::traits::FitnessValue;
10use crate::genome::traits::EvolutionaryGenome;
11
12/// An individual in the population
13///
14/// Wraps a genome with its computed fitness value and additional metadata.
15#[derive(Clone, Debug, Serialize, Deserialize)]
16#[serde(bound = "")]
17pub struct Individual<G, F = f64>
18where
19    G: EvolutionaryGenome,
20    F: FitnessValue,
21{
22    /// The genome of this individual
23    pub genome: G,
24    /// The fitness value (None if not yet evaluated)
25    pub fitness: Option<F>,
26    /// Generation when this individual was created
27    pub birth_generation: usize,
28    /// Number of offspring produced by this individual
29    pub offspring_count: usize,
30}
31
32impl<G, F> Individual<G, F>
33where
34    G: EvolutionaryGenome,
35    F: FitnessValue,
36{
37    /// Create a new individual with an unevaluated genome
38    pub fn new(genome: G) -> Self {
39        Self {
40            genome,
41            fitness: None,
42            birth_generation: 0,
43            offspring_count: 0,
44        }
45    }
46
47    /// Create a new individual with a known fitness
48    pub fn with_fitness(genome: G, fitness: F) -> Self {
49        Self {
50            genome,
51            fitness: Some(fitness),
52            birth_generation: 0,
53            offspring_count: 0,
54        }
55    }
56
57    /// Create a new individual with birth generation
58    pub fn with_generation(genome: G, generation: usize) -> Self {
59        Self {
60            genome,
61            fitness: None,
62            birth_generation: generation,
63            offspring_count: 0,
64        }
65    }
66
67    /// Check if this individual has been evaluated
68    pub fn is_evaluated(&self) -> bool {
69        self.fitness.is_some()
70    }
71
72    /// Get the fitness value, panicking if not evaluated
73    pub fn fitness_value(&self) -> &F {
74        self.fitness
75            .as_ref()
76            .expect("Individual has not been evaluated")
77    }
78
79    /// Get the fitness as f64
80    pub fn fitness_f64(&self) -> f64 {
81        self.fitness_value().to_f64()
82    }
83
84    /// Set the fitness value
85    ///
86    /// # Panics
87    ///
88    /// Panics if the fitness is non-finite (`NaN`). A `NaN` fitness is always a
89    /// bug in the fitness function (e.g. a division by zero or `log` of a
90    /// negative number) and silently corrupts every downstream comparison
91    /// (`best`/`worst`/`sort_by_fitness`), so it is rejected at the source
92    /// rather than propagated (EV-07). Infinities are permitted (e.g.
93    /// `ParetoFitness` uses `+inf` crowding distances).
94    pub fn set_fitness(&mut self, fitness: F) {
95        assert!(
96            !fitness.to_f64().is_nan(),
97            "Individual::set_fitness received a NaN fitness value; \
98             fitness functions must return a finite (non-NaN) value"
99        );
100        self.fitness = Some(fitness);
101    }
102
103    /// Take the genome out of this individual
104    pub fn into_genome(self) -> G {
105        self.genome
106    }
107
108    /// Get a reference to the genome
109    pub fn genome(&self) -> &G {
110        &self.genome
111    }
112
113    /// Get a mutable reference to the genome
114    ///
115    /// Mutating the genome invalidates any cached fitness (EV-28): the stored
116    /// value was computed for the *previous* genome, so this clears
117    /// `self.fitness` and resets the evaluated flag. Callers that only need
118    /// read access should use [`genome`](Self::genome) to preserve the cache.
119    ///
120    /// Note: the `genome` field is public for backwards compatibility; direct
121    /// assignment (`individual.genome = ...`) bypasses this invalidation, so
122    /// prefer [`genome_mut`](Self::genome_mut) or [`set_genome`](Self::set_genome).
123    pub fn genome_mut(&mut self) -> &mut G {
124        self.fitness = None;
125        &mut self.genome
126    }
127
128    /// Replace the genome, clearing any cached fitness
129    ///
130    /// Like [`genome_mut`](Self::genome_mut), this invalidates the cached
131    /// fitness so the new genome is re-evaluated (EV-28).
132    pub fn set_genome(&mut self, genome: G) {
133        self.genome = genome;
134        self.fitness = None;
135    }
136
137    /// Check if this individual is better than another
138    pub fn is_better_than(&self, other: &Self) -> bool {
139        match (&self.fitness, &other.fitness) {
140            (Some(f1), Some(f2)) => f1.is_better_than(f2),
141            (Some(_), None) => true,
142            (None, Some(_)) => false,
143            (None, None) => false,
144        }
145    }
146
147    /// Age of this individual (generations since birth)
148    pub fn age(&self, current_generation: usize) -> usize {
149        current_generation.saturating_sub(self.birth_generation)
150    }
151}
152
153impl<G, F> PartialEq for Individual<G, F>
154where
155    G: EvolutionaryGenome + PartialEq,
156    F: FitnessValue + PartialEq,
157{
158    fn eq(&self, other: &Self) -> bool {
159        self.genome == other.genome && self.fitness == other.fitness
160    }
161}
162
163impl<G, F> PartialOrd for Individual<G, F>
164where
165    G: EvolutionaryGenome + PartialEq,
166    F: FitnessValue + PartialEq,
167{
168    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
169        match (&self.fitness, &other.fitness) {
170            (Some(f1), Some(f2)) => f1.partial_cmp(f2),
171            (Some(_), None) => Some(Ordering::Greater),
172            (None, Some(_)) => Some(Ordering::Less),
173            (None, None) => Some(Ordering::Equal),
174        }
175    }
176}
177
178/// A pair of individuals (for crossover results)
179pub type IndividualPair<G, F = f64> = (Individual<G, F>, Individual<G, F>);
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::genome::real_vector::RealVector;
185    use crate::genome::traits::RealValuedGenome;
186
187    #[test]
188    fn test_individual_new() {
189        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
190        let individual: Individual<RealVector> = Individual::new(genome);
191
192        assert!(!individual.is_evaluated());
193        assert_eq!(individual.birth_generation, 0);
194        assert_eq!(individual.offspring_count, 0);
195    }
196
197    #[test]
198    fn test_individual_with_fitness() {
199        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
200        let individual = Individual::with_fitness(genome, 42.0);
201
202        assert!(individual.is_evaluated());
203        assert_eq!(individual.fitness_f64(), 42.0);
204    }
205
206    #[test]
207    fn test_individual_set_fitness() {
208        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
209        let mut individual: Individual<RealVector> = Individual::new(genome);
210
211        assert!(!individual.is_evaluated());
212        individual.set_fitness(100.0);
213        assert!(individual.is_evaluated());
214        assert_eq!(individual.fitness_f64(), 100.0);
215    }
216
217    #[test]
218    fn test_individual_is_better_than() {
219        let g1 = RealVector::new(vec![1.0]);
220        let g2 = RealVector::new(vec![2.0]);
221
222        let ind1 = Individual::with_fitness(g1, 100.0);
223        let ind2 = Individual::with_fitness(g2, 50.0);
224
225        assert!(ind1.is_better_than(&ind2));
226        assert!(!ind2.is_better_than(&ind1));
227    }
228
229    #[test]
230    fn test_individual_is_better_than_unevaluated() {
231        let g1 = RealVector::new(vec![1.0]);
232        let g2 = RealVector::new(vec![2.0]);
233
234        let ind1 = Individual::with_fitness(g1, 100.0);
235        let ind2: Individual<RealVector> = Individual::new(g2);
236
237        assert!(ind1.is_better_than(&ind2));
238        assert!(!ind2.is_better_than(&ind1));
239    }
240
241    #[test]
242    fn test_individual_age() {
243        let genome = RealVector::new(vec![1.0]);
244        let individual: Individual<RealVector> = Individual::with_generation(genome, 10);
245
246        assert_eq!(individual.age(10), 0);
247        assert_eq!(individual.age(15), 5);
248        assert_eq!(individual.age(5), 0); // saturating sub
249    }
250
251    #[test]
252    fn test_individual_partial_ord() {
253        let g1 = RealVector::new(vec![1.0]);
254        let g2 = RealVector::new(vec![2.0]);
255
256        let ind1 = Individual::with_fitness(g1, 100.0);
257        let ind2 = Individual::with_fitness(g2, 50.0);
258
259        assert!(ind1 > ind2);
260        assert!(ind2 < ind1);
261    }
262
263    #[test]
264    fn test_individual_into_genome() {
265        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
266        let individual = Individual::with_fitness(genome.clone(), 42.0);
267
268        let recovered = individual.into_genome();
269        assert_eq!(recovered, genome);
270    }
271
272    #[test]
273    fn test_individual_genome_mut() {
274        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
275        let mut individual: Individual<RealVector> = Individual::new(genome);
276
277        individual.genome_mut().genes_mut()[0] = 100.0;
278        assert_eq!(individual.genome()[0], 100.0);
279    }
280
281    #[test]
282    #[should_panic(expected = "NaN")]
283    fn test_set_fitness_rejects_nan() {
284        // regression: EV-07 -- reject non-finite fitness at the source.
285        let mut individual: Individual<RealVector> = Individual::new(RealVector::new(vec![1.0]));
286        individual.set_fitness(f64::NAN);
287    }
288
289    #[test]
290    fn test_genome_mut_clears_cached_fitness() {
291        // regression: EV-28 -- mutating the genome must invalidate the cache.
292        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
293        let mut individual = Individual::with_fitness(genome, 42.0);
294        assert!(individual.is_evaluated());
295
296        individual.genome_mut().genes_mut()[0] = 100.0;
297        assert!(
298            !individual.is_evaluated(),
299            "cached fitness must be cleared after genome_mut()"
300        );
301    }
302
303    #[test]
304    fn test_set_genome_clears_cached_fitness() {
305        // regression: EV-28 -- set_genome must invalidate the cache too.
306        let mut individual = Individual::with_fitness(RealVector::new(vec![1.0]), 42.0);
307        assert!(individual.is_evaluated());
308
309        individual.set_genome(RealVector::new(vec![2.0]));
310        assert!(!individual.is_evaluated());
311        assert_eq!(individual.genome()[0], 2.0);
312    }
313}