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