Skip to main content

fugue_evo/fitness/
traits.rs

1//! Fitness traits
2//!
3//! This module defines the fitness evaluation traits.
4
5use std::fmt::Debug;
6
7use serde::{de::DeserializeOwned, Serialize};
8
9use crate::genome::traits::EvolutionaryGenome;
10
11/// Trait bound for fitness values
12///
13/// Fitness values must be comparable and convertible to f64 for
14/// probabilistic selection operations. They must also be serializable
15/// for checkpointing.
16pub trait FitnessValue:
17    PartialOrd + Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static
18{
19    /// Convert fitness to f64 for probabilistic operations
20    fn to_f64(&self) -> f64;
21
22    /// Check if this fitness is better than another
23    fn is_better_than(&self, other: &Self) -> bool;
24
25    /// Check if this fitness is worse than another
26    fn is_worse_than(&self, other: &Self) -> bool {
27        other.is_better_than(self)
28    }
29
30    /// Total ordering by quality, where [`Ordering::Greater`] means `self` is
31    /// the better individual.
32    ///
33    /// This delegates to [`is_better_than`](FitnessValue::is_better_than) for
34    /// the quality comparison (never to a `to_f64()`-derived scalar, which can
35    /// disagree with the true ordering — see the `ParetoFitness` case where
36    /// infinite crowding distances collapse every rank to `+inf`). Any value
37    /// whose `to_f64()` is `NaN` is ranked strictly worst, so the result is a
38    /// genuine total order usable with `max_by`/`min_by`/`sort_by` even if a
39    /// `NaN` fitness slips past [`Individual::set_fitness`]'s guard (defense in
40    /// depth).
41    ///
42    /// [`Ordering::Greater`]: std::cmp::Ordering::Greater
43    /// [`Individual::set_fitness`]: crate::population::individual::Individual::set_fitness
44    fn cmp_by_quality(&self, other: &Self) -> std::cmp::Ordering {
45        use std::cmp::Ordering;
46        let self_nan = self.to_f64().is_nan();
47        let other_nan = other.to_f64().is_nan();
48        match (self_nan, other_nan) {
49            (true, true) => Ordering::Equal,
50            // A NaN is strictly worse than any real fitness.
51            (true, false) => Ordering::Less,
52            (false, true) => Ordering::Greater,
53            (false, false) => {
54                if self.is_better_than(other) {
55                    Ordering::Greater
56                } else if other.is_better_than(self) {
57                    Ordering::Less
58                } else {
59                    Ordering::Equal
60                }
61            }
62        }
63    }
64}
65
66impl FitnessValue for f64 {
67    fn to_f64(&self) -> f64 {
68        *self
69    }
70
71    fn is_better_than(&self, other: &Self) -> bool {
72        self > other
73    }
74}
75
76impl FitnessValue for f32 {
77    fn to_f64(&self) -> f64 {
78        *self as f64
79    }
80
81    fn is_better_than(&self, other: &Self) -> bool {
82        self > other
83    }
84}
85
86impl FitnessValue for i64 {
87    fn to_f64(&self) -> f64 {
88        *self as f64
89    }
90
91    fn is_better_than(&self, other: &Self) -> bool {
92        self > other
93    }
94}
95
96impl FitnessValue for i32 {
97    fn to_f64(&self) -> f64 {
98        *self as f64
99    }
100
101    fn is_better_than(&self, other: &Self) -> bool {
102        self > other
103    }
104}
105
106impl FitnessValue for usize {
107    fn to_f64(&self) -> f64 {
108        *self as f64
109    }
110
111    fn is_better_than(&self, other: &Self) -> bool {
112        self > other
113    }
114}
115
116/// Multi-objective fitness value using Pareto ranking
117#[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)]
118pub struct ParetoFitness {
119    /// Objective values (all to be maximized)
120    pub objectives: Vec<f64>,
121    /// Pareto rank (0 = non-dominated front)
122    pub rank: usize,
123    /// Crowding distance for diversity preservation
124    pub crowding_distance: f64,
125}
126
127impl ParetoFitness {
128    /// Create a new Pareto fitness with the given objectives
129    pub fn new(objectives: Vec<f64>) -> Self {
130        Self {
131            objectives,
132            rank: usize::MAX,
133            crowding_distance: 0.0,
134        }
135    }
136
137    /// Check if this solution dominates another
138    /// (all objectives >= and at least one >)
139    pub fn dominates(&self, other: &Self) -> bool {
140        let dominated = self
141            .objectives
142            .iter()
143            .zip(other.objectives.iter())
144            .all(|(a, b)| a >= b);
145        let strictly_better = self
146            .objectives
147            .iter()
148            .zip(other.objectives.iter())
149            .any(|(a, b)| a > b);
150        dominated && strictly_better
151    }
152
153    /// Number of objectives
154    pub fn num_objectives(&self) -> usize {
155        self.objectives.len()
156    }
157}
158
159impl PartialOrd for ParetoFitness {
160    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
161        // Compare by rank first, then by crowding distance
162        match self.rank.partial_cmp(&other.rank) {
163            Some(std::cmp::Ordering::Equal) => {
164                // Higher crowding distance is better (more diverse)
165                self.crowding_distance.partial_cmp(&other.crowding_distance)
166            }
167            ord => ord.map(|o| o.reverse()), // Reverse because lower rank is better
168        }
169    }
170}
171
172impl FitnessValue for ParetoFitness {
173    fn to_f64(&self) -> f64 {
174        // Aggregated scalar for probabilistic interpretation
175        // Lower rank is better, so negate it
176        -(self.rank as f64) + self.crowding_distance * 0.001
177    }
178
179    fn is_better_than(&self, other: &Self) -> bool {
180        self.rank < other.rank
181            || (self.rank == other.rank && self.crowding_distance > other.crowding_distance)
182    }
183}
184
185/// Fitness evaluation trait
186///
187/// Defines how to evaluate the fitness of a genome.
188#[cfg(feature = "parallel")]
189pub trait Fitness: Send + Sync {
190    /// The genome type being evaluated
191    type Genome: EvolutionaryGenome;
192
193    /// The fitness value type
194    type Value: FitnessValue;
195
196    /// Evaluate fitness (higher = better by convention)
197    fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
198
199    /// Convert fitness to log-likelihood for probabilistic selection
200    ///
201    /// Uses Boltzmann distribution: P(x) ∝ exp(f(x) / T)
202    fn as_log_likelihood(&self, genome: &Self::Genome, temperature: f64) -> f64 {
203        let fitness = self.evaluate(genome).to_f64();
204        fitness / temperature
205    }
206
207    /// Optional: Provide gradient for gradient-assisted mutation
208    fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
209        None
210    }
211}
212
213/// Fitness evaluation trait (non-parallel version)
214///
215/// Defines how to evaluate the fitness of a genome.
216#[cfg(not(feature = "parallel"))]
217pub trait Fitness {
218    /// The genome type being evaluated
219    type Genome: EvolutionaryGenome;
220
221    /// The fitness value type
222    type Value: FitnessValue;
223
224    /// Evaluate fitness (higher = better by convention)
225    fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
226
227    /// Convert fitness to log-likelihood for probabilistic selection
228    ///
229    /// Uses Boltzmann distribution: P(x) ∝ exp(f(x) / T)
230    fn as_log_likelihood(&self, genome: &Self::Genome, temperature: f64) -> f64 {
231        let fitness = self.evaluate(genome).to_f64();
232        fitness / temperature
233    }
234
235    /// Optional: Provide gradient for gradient-assisted mutation
236    fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
237        None
238    }
239}
240
241/// A wrapper to negate a fitness function (for minimization problems)
242pub struct MinimizeFitness<F> {
243    inner: F,
244}
245
246impl<F> MinimizeFitness<F> {
247    /// Create a minimization wrapper around a fitness function
248    pub fn new(fitness: F) -> Self {
249        Self { inner: fitness }
250    }
251}
252
253impl<F: Fitness<Value = f64>> Fitness for MinimizeFitness<F> {
254    type Genome = F::Genome;
255    type Value = f64;
256
257    fn evaluate(&self, genome: &Self::Genome) -> f64 {
258        -self.inner.evaluate(genome)
259    }
260}
261
262/// A simple function wrapper for fitness evaluation
263pub struct FnFitness<G, F, V>
264where
265    F: Fn(&G) -> V,
266{
267    f: F,
268    _marker: std::marker::PhantomData<(G, V)>,
269}
270
271impl<G, F, V> FnFitness<G, F, V>
272where
273    F: Fn(&G) -> V,
274{
275    /// Create a new function-based fitness evaluator
276    pub fn new(f: F) -> Self {
277        Self {
278            f,
279            _marker: std::marker::PhantomData,
280        }
281    }
282}
283
284impl<G, F, V> Fitness for FnFitness<G, F, V>
285where
286    G: EvolutionaryGenome,
287    F: Fn(&G) -> V + Send + Sync,
288    V: FitnessValue,
289{
290    type Genome = G;
291    type Value = V;
292
293    fn evaluate(&self, genome: &Self::Genome) -> Self::Value {
294        (self.f)(genome)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::genome::real_vector::RealVector;
302    use crate::genome::traits::RealValuedGenome;
303
304    #[test]
305    fn test_f64_fitness_value() {
306        let a: f64 = 10.0;
307        let b: f64 = 5.0;
308
309        assert!(a.is_better_than(&b));
310        assert!(!b.is_better_than(&a));
311        assert!(b.is_worse_than(&a));
312        assert_eq!(a.to_f64(), 10.0);
313    }
314
315    #[test]
316    fn test_i32_fitness_value() {
317        let a: i32 = 10;
318        let b: i32 = 5;
319
320        assert!(a.is_better_than(&b));
321        assert!(!b.is_better_than(&a));
322        assert_eq!(a.to_f64(), 10.0);
323    }
324
325    #[test]
326    fn test_usize_fitness_value() {
327        let a: usize = 10;
328        let b: usize = 5;
329
330        assert!(a.is_better_than(&b));
331        assert!(!b.is_better_than(&a));
332        assert_eq!(a.to_f64(), 10.0);
333    }
334
335    #[test]
336    fn test_pareto_fitness_dominates() {
337        let a = ParetoFitness::new(vec![5.0, 5.0]);
338        let b = ParetoFitness::new(vec![3.0, 3.0]);
339        let c = ParetoFitness::new(vec![6.0, 3.0]); // Better in one, worse in other - not dominated by a
340
341        assert!(a.dominates(&b)); // a is better in all objectives
342        assert!(!b.dominates(&a)); // b is worse in all objectives
343        assert!(!a.dominates(&c)); // c is better in first objective, so not dominated
344        assert!(!c.dominates(&a)); // a is better in second objective, so c doesn't dominate a
345    }
346
347    #[test]
348    fn test_pareto_fitness_is_better_than() {
349        let mut a = ParetoFitness::new(vec![5.0, 5.0]);
350        a.rank = 0;
351        a.crowding_distance = 1.0;
352
353        let mut b = ParetoFitness::new(vec![3.0, 3.0]);
354        b.rank = 1;
355        b.crowding_distance = 2.0;
356
357        assert!(a.is_better_than(&b)); // Lower rank is better
358
359        let mut c = ParetoFitness::new(vec![4.0, 4.0]);
360        c.rank = 0;
361        c.crowding_distance = 0.5;
362
363        assert!(a.is_better_than(&c)); // Same rank, higher crowding distance
364    }
365
366    #[test]
367    fn test_fn_fitness() {
368        let fitness = FnFitness::new(|g: &RealVector| -> f64 {
369            -g.genes().iter().map(|x| x * x).sum::<f64>()
370        });
371
372        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
373        let value = fitness.evaluate(&genome);
374        assert_eq!(value, -14.0);
375    }
376
377    #[test]
378    fn test_minimize_fitness() {
379        let fitness = FnFitness::new(|g: &RealVector| -> f64 {
380            g.genes().iter().map(|x| x * x).sum::<f64>()
381        });
382        let minimize = MinimizeFitness::new(fitness);
383
384        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
385        let value = minimize.evaluate(&genome);
386        assert_eq!(value, -14.0);
387    }
388
389    #[test]
390    fn test_as_log_likelihood() {
391        let fitness = FnFitness::new(|g: &RealVector| -> f64 {
392            -g.genes().iter().map(|x| x * x).sum::<f64>()
393        });
394
395        let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
396        let log_likelihood = fitness.as_log_likelihood(&genome, 1.0);
397        assert_eq!(log_likelihood, -14.0);
398
399        let log_likelihood_scaled = fitness.as_log_likelihood(&genome, 2.0);
400        assert_eq!(log_likelihood_scaled, -7.0);
401    }
402}