1use phago_core::types::AgentId;
8use serde::Serialize;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize)]
13pub struct AgentFitness {
14 pub agent_id: AgentId,
15 pub concepts_added: u64,
17 pub edges_contributed: u64,
19 pub ticks_alive: u64,
21 pub fitness: f64,
23 pub generation: u32,
25 pub novel_concepts: u64,
27 pub bridge_edges: u64,
29 pub strong_edges: u64,
31}
32
33pub struct FitnessTracker {
35 data: HashMap<AgentId, AgentFitness>,
36 generation_counter: u32,
37}
38
39impl FitnessTracker {
40 pub fn new() -> Self {
41 Self {
42 data: HashMap::new(),
43 generation_counter: 0,
44 }
45 }
46
47 pub fn register(&mut self, agent_id: AgentId, generation: u32) {
49 self.data.insert(agent_id, AgentFitness {
50 agent_id,
51 concepts_added: 0,
52 edges_contributed: 0,
53 ticks_alive: 0,
54 fitness: 0.0,
55 generation,
56 novel_concepts: 0,
57 bridge_edges: 0,
58 strong_edges: 0,
59 });
60 }
61
62 pub fn record_concepts(&mut self, agent_id: &AgentId, count: u64) {
64 if let Some(f) = self.data.get_mut(agent_id) {
65 f.concepts_added += count;
66 Self::recompute_fitness(f);
67 }
68 }
69
70 pub fn record_edges(&mut self, agent_id: &AgentId, count: u64) {
72 if let Some(f) = self.data.get_mut(agent_id) {
73 f.edges_contributed += count;
74 Self::recompute_fitness(f);
75 }
76 }
77
78 pub fn record_novel_concepts(&mut self, agent_id: &AgentId, count: u64) {
80 if let Some(f) = self.data.get_mut(agent_id) {
81 f.novel_concepts += count;
82 Self::recompute_fitness(f);
83 }
84 }
85
86 pub fn record_bridge_edges(&mut self, agent_id: &AgentId, count: u64) {
88 if let Some(f) = self.data.get_mut(agent_id) {
89 f.bridge_edges += count;
90 Self::recompute_fitness(f);
91 }
92 }
93
94 pub fn record_strong_edges(&mut self, agent_id: &AgentId, count: u64) {
96 if let Some(f) = self.data.get_mut(agent_id) {
97 f.strong_edges += count;
98 Self::recompute_fitness(f);
99 }
100 }
101
102 pub fn tick_all(&mut self, alive_ids: &[AgentId]) {
104 for id in alive_ids {
105 if let Some(f) = self.data.get_mut(id) {
106 f.ticks_alive += 1;
107 Self::recompute_fitness(f);
108 }
109 }
110 }
111
112 fn recompute_fitness(f: &mut AgentFitness) {
120 if f.ticks_alive == 0 {
121 return;
122 }
123
124 let productivity = (f.concepts_added as f64 + f.edges_contributed as f64)
125 / f.ticks_alive as f64;
126
127 let novelty = if f.concepts_added > 0 {
128 f.novel_concepts as f64 / f.concepts_added as f64
129 } else {
130 0.0
131 };
132
133 let quality = if f.edges_contributed > 0 {
134 f.strong_edges as f64 / f.edges_contributed as f64
135 } else {
136 0.0
137 };
138
139 let connectivity = if f.edges_contributed > 0 {
140 f.bridge_edges as f64 / f.edges_contributed as f64
141 } else {
142 0.0
143 };
144
145 f.fitness = 0.3 * productivity + 0.3 * novelty + 0.2 * quality + 0.2 * connectivity;
146 }
147
148 pub fn fittest(&self, alive_ids: &[AgentId]) -> Option<&AgentFitness> {
150 alive_ids.iter()
151 .filter_map(|id| self.data.get(id))
152 .max_by(|a, b| a.fitness.partial_cmp(&b.fitness).unwrap_or(std::cmp::Ordering::Equal))
153 }
154
155 pub fn get(&self, agent_id: &AgentId) -> Option<&AgentFitness> {
157 self.data.get(agent_id)
158 }
159
160 pub fn all(&self) -> Vec<&AgentFitness> {
162 self.data.values().collect()
163 }
164
165 pub fn mean_fitness(&self, alive_ids: &[AgentId]) -> f64 {
167 let fitnesses: Vec<f64> = alive_ids.iter()
168 .filter_map(|id| self.data.get(id))
169 .map(|f| f.fitness)
170 .collect();
171 if fitnesses.is_empty() {
172 0.0
173 } else {
174 fitnesses.iter().sum::<f64>() / fitnesses.len() as f64
175 }
176 }
177
178 pub fn next_generation(&mut self) -> u32 {
180 self.generation_counter += 1;
181 self.generation_counter
182 }
183
184 pub fn max_generation(&self) -> u32 {
186 self.data.values().map(|f| f.generation).max().unwrap_or(0)
187 }
188}
189
190impl Default for FitnessTracker {
191 fn default() -> Self {
192 Self::new()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn fitness_tracks_contributions() {
202 let mut tracker = FitnessTracker::new();
203 let id = AgentId::new();
204 tracker.register(id, 0);
205 tracker.record_concepts(&id, 5);
206 tracker.tick_all(&[id]);
207
208 let f = tracker.get(&id).unwrap();
209 assert_eq!(f.concepts_added, 5);
210 assert_eq!(f.ticks_alive, 1);
211 assert!(f.fitness > 0.0);
212 }
213
214 #[test]
215 fn fittest_returns_best_agent() {
216 let mut tracker = FitnessTracker::new();
217 let id1 = AgentId::new();
218 let id2 = AgentId::new();
219 tracker.register(id1, 0);
220 tracker.register(id2, 0);
221 tracker.record_concepts(&id1, 10);
222 tracker.record_concepts(&id2, 2);
223 tracker.tick_all(&[id1, id2]);
224
225 let best = tracker.fittest(&[id1, id2]).unwrap();
226 assert_eq!(best.agent_id, id1);
227 }
228}