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
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#[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)]
83pub struct ParetoFitness {
84 pub objectives: Vec<f64>,
86 pub rank: usize,
88 pub crowding_distance: f64,
90}
91
92impl ParetoFitness {
93 pub fn new(objectives: Vec<f64>) -> Self {
95 Self {
96 objectives,
97 rank: usize::MAX,
98 crowding_distance: 0.0,
99 }
100 }
101
102 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 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 match self.rank.partial_cmp(&other.rank) {
128 Some(std::cmp::Ordering::Equal) => {
129 self.crowding_distance.partial_cmp(&other.crowding_distance)
131 }
132 ord => ord.map(|o| o.reverse()), }
134 }
135}
136
137impl FitnessValue for ParetoFitness {
138 fn to_f64(&self) -> f64 {
139 -(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#[cfg(feature = "parallel")]
154pub trait Fitness: Send + Sync {
155 type Genome: EvolutionaryGenome;
157
158 type Value: FitnessValue;
160
161 fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
163
164 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 fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
174 None
175 }
176}
177
178#[cfg(not(feature = "parallel"))]
182pub trait Fitness {
183 type Genome: EvolutionaryGenome;
185
186 type Value: FitnessValue;
188
189 fn evaluate(&self, genome: &Self::Genome) -> Self::Value;
191
192 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 fn gradient(&self, _genome: &Self::Genome) -> Option<Vec<f64>> {
202 None
203 }
204}
205
206pub struct MinimizeFitness<F> {
208 inner: F,
209}
210
211impl<F> MinimizeFitness<F> {
212 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
227pub 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 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]); assert!(a.dominates(&b)); assert!(!b.dominates(&a)); assert!(!a.dominates(&c)); assert!(!c.dominates(&a)); }
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)); 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)); }
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}