fugue_evo/fitness/
traits.rs1use std::fmt::Debug;
6
7use serde::{de::DeserializeOwned, Serialize};
8
9use crate::genome::traits::EvolutionaryGenome;
10
11pub trait FitnessValue:
17 PartialOrd + Clone + Send + Sync + Debug + Serialize + DeserializeOwned + 'static
18{
19 fn to_f64(&self) -> f64;
21
22 fn is_better_than(&self, other: &Self) -> bool;
24
25 fn is_worse_than(&self, other: &Self) -> bool {
27 other.is_better_than(self)
28 }
29
30 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 (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#[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)]
118pub struct ParetoFitness {
119 pub objectives: Vec<f64>,
121 pub rank: usize,
123 pub crowding_distance: f64,
125}
126
127impl ParetoFitness {
128 pub fn new(objectives: Vec<f64>) -> Self {
130 Self {
131 objectives,
132 rank: usize::MAX,
133 crowding_distance: 0.0,
134 }
135 }
136
137 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 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 match self.rank.partial_cmp(&other.rank) {
163 Some(std::cmp::Ordering::Equal) => {
164 self.crowding_distance.partial_cmp(&other.crowding_distance)
166 }
167 ord => ord.map(|o| o.reverse()), }
169 }
170}
171
172impl FitnessValue for ParetoFitness {
173 fn to_f64(&self) -> f64 {
174 -(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#[cfg(feature = "parallel")]
189pub trait Fitness: Send + Sync {
190 type Genome: EvolutionaryGenome;
192
193 type Value: FitnessValue;
195
196 fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
198
199 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 fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
209 None
210 }
211}
212
213#[cfg(not(feature = "parallel"))]
217pub trait Fitness {
218 type Genome: EvolutionaryGenome;
220
221 type Value: FitnessValue;
223
224 fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
226
227 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 fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
237 None
238 }
239}
240
241pub struct MinimizeFitness<F> {
243 inner: F,
244}
245
246impl<F> MinimizeFitness<F> {
247 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
262pub 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 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]); assert!(a.dominates(&b)); assert!(!b.dominates(&a)); assert!(!a.dominates(&c)); assert!(!c.dominates(&a)); }
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)); 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)); }
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}