Skip to main content

evolution_ternary/
fitness.rs

1//! Fitness functions and multi-environment support.
2
3/// A named environment with its own fitness weights.
4#[derive(Clone, Debug)]
5pub struct Environment {
6    pub name: String,
7    /// Per-position weights. Length should match strategy vector length.
8    /// For each position, weights[0] = value of 0, weights[1] = value of 1, weights[2] = value of 2.
9    pub weights: Vec<[f64; 3]>,
10}
11
12impl Environment {
13    /// Create a simple environment where each position values 2 > 1 > 0.
14    pub fn simple(len: usize) -> Self {
15        Self {
16            name: "simple".into(),
17            weights: vec![[0.0, 1.0, 2.0]; len],
18        }
19    }
20
21    /// An environment that rewards the "diagonal" pattern: position i prefers value i%3.
22    pub fn diagonal(len: usize) -> Self {
23        let weights: Vec<[f64; 3]> = (0..len)
24            .map(|i| {
25                let mut w = [0.0, 0.0, 0.0];
26                w[i % 3] = 2.0;
27                // Give small fitness to others so no zero fitness
28                w[(i + 1) % 3] = 0.5;
29                w[(i + 2) % 3] = 0.5;
30                w
31            })
32            .collect();
33        Self { name: "diagonal".into(), weights }
34    }
35
36    /// All zeros preferred.
37    pub fn zeros(len: usize) -> Self {
38        Self {
39            name: "zeros".into(),
40            weights: vec![[2.0, 0.5, 0.5]; len],
41        }
42    }
43
44    /// Random environment with given seed.
45    pub fn random_seeded(len: usize, seed: u64) -> Self {
46        use crate::population::SimpleRng;
47        let mut rng = SimpleRng::from_seed(seed);
48        let weights: Vec<[f64; 3]> = (0..len)
49            .map(|_| {
50                let mut w = [0.0; 3];
51                for v in &mut w {
52                    *v = rng.next_f64();
53                }
54                w
55            })
56            .collect();
57        Self { name: format!("random-{}", seed), weights }
58    }
59
60    /// Evaluate the fitness of a strategy in this environment.
61    pub fn evaluate(&self, strategy: &[u8]) -> f64 {
62        assert_eq!(strategy.len(), self.weights.len(), "Strategy length must match environment");
63        strategy.iter().zip(self.weights.iter())
64            .map(|(&v, w)| w[v as usize])
65            .sum()
66    }
67}
68
69/// A fitness function that may combine multiple environments.
70#[derive(Clone, Debug)]
71pub struct FitnessFunction {
72    pub environments: Vec<Environment>,
73    /// How to combine multi-environment fitness: "sum", "max", or "avg".
74    pub combine: String,
75}
76
77impl FitnessFunction {
78    /// Single-environment fitness function.
79    pub fn single(env: Environment) -> Self {
80        Self { environments: vec![env], combine: "sum".into() }
81    }
82
83    /// Multi-environment with sum combination.
84    pub fn multi_sum(envs: Vec<Environment>) -> Self {
85        Self { environments: envs, combine: "sum".into() }
86    }
87
88    /// Multi-environment with max combination.
89    pub fn multi_max(envs: Vec<Environment>) -> Self {
90        Self { environments: envs, combine: "max".into() }
91    }
92
93    /// Evaluate fitness across all environments.
94    pub fn evaluate(&self, strategy: &[u8]) -> f64 {
95        let scores: Vec<f64> = self.environments.iter()
96            .map(|e| e.evaluate(strategy))
97            .collect();
98        match self.combine.as_str() {
99            "sum" => scores.iter().sum(),
100            "max" => scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
101            "avg" => scores.iter().sum::<f64>() / scores.len().max(1) as f64,
102            _ => scores.iter().sum(),
103        }
104    }
105}