1use std::collections::{HashMap, VecDeque};
9
10use crate::evolution::Genome;
11use crate::genome_experiment::GenomeExperiment;
12
13#[derive(Debug, Clone, Default)]
24pub struct EvolutionMemory {
25 experiments: VecDeque<(GenomeExperiment, f64)>, max_entries: usize,
29 correlations: HashMap<String, f64>,
31}
32
33impl EvolutionMemory {
34 pub fn new() -> Self {
36 Self {
37 experiments: VecDeque::new(),
38 max_entries: 500,
39 correlations: HashMap::new(),
40 }
41 }
42
43 pub fn with_capacity(max_entries: usize) -> Self {
45 let safe_capacity = max_entries.min(10_000);
47 Self {
48 experiments: VecDeque::with_capacity(safe_capacity.min(100)),
49 max_entries: safe_capacity,
50 correlations: HashMap::new(),
51 }
52 }
53
54 pub fn record(&mut self, experiment: GenomeExperiment) {
61 let importance = experiment.overall_fitness;
63
64 self.experiments.push_front((experiment, importance));
66
67 metrics::counter!("vex_experiments_recorded_total").increment(1);
68
69 self.maybe_evict();
71
72 if self.experiments.len() % 10 == 0 {
74 self.update_correlations();
75 }
76 }
77
78 pub fn get_top_experiments(&self, limit: usize) -> Vec<&GenomeExperiment> {
80 let mut sorted: Vec<_> = self.experiments.iter().collect();
81 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
82 sorted.into_iter().take(limit).map(|(exp, _)| exp).collect()
83 }
84
85 pub fn experiments(&self) -> impl Iterator<Item = &GenomeExperiment> {
87 self.experiments.iter().map(|(exp, _)| exp)
88 }
89
90 pub fn len(&self) -> usize {
92 self.experiments.len()
93 }
94
95 pub fn is_empty(&self) -> bool {
97 self.experiments.is_empty()
98 }
99
100 pub fn correlations(&self) -> &HashMap<String, f64> {
102 &self.correlations
103 }
104
105 pub fn get_experiments_snapshot(&self) -> Vec<GenomeExperiment> {
107 self.experiments
108 .iter()
109 .map(|(exp, _)| exp.clone())
110 .collect()
111 }
112
113 pub fn get_experiments_oldest(&self, count: usize) -> Vec<GenomeExperiment> {
115 self.experiments
116 .iter()
117 .take(count)
118 .map(|(exp, _)| exp.clone())
119 .collect()
120 }
121
122 pub fn clear(&mut self) {
125 self.experiments.clear();
126 metrics::gauge!("vex_evolution_memory_size").set(0.0);
127 }
128
129 pub fn drain_oldest(&mut self, count: usize) {
131 let actual_count = count.min(self.experiments.len());
132 self.experiments.drain(0..actual_count);
133 metrics::gauge!("vex_evolution_memory_size").set(self.experiments.len() as f64);
134 }
135
136 fn update_correlations(&mut self) {
138 if self.experiments.len() < 10 {
139 return;
140 }
141
142 let trait_count = self
143 .experiments
144 .front()
145 .map(|(e, _)| e.traits.len())
146 .unwrap_or(5);
147
148 for i in 0..trait_count {
149 let trait_name = self
150 .experiments
151 .front()
152 .and_then(|(e, _)| e.trait_names.get(i).cloned())
153 .unwrap_or_else(|| format!("trait_{}", i));
154
155 let trait_values: Vec<f64> = self
156 .experiments
157 .iter()
158 .filter_map(|(e, _)| e.traits.get(i).copied())
159 .collect();
160 let fitness_values: Vec<f64> = self
161 .experiments
162 .iter()
163 .map(|(e, _)| e.overall_fitness)
164 .collect();
165
166 if trait_values.len() >= 10 {
167 let corr = pearson_correlation(&trait_values, &fitness_values);
168 self.correlations.insert(trait_name, corr);
169 }
170 }
171
172 metrics::gauge!("vex_learned_correlations_count").set(self.correlations.len() as f64);
173 }
174
175 pub fn suggest_adjustments(&self, current: &Genome) -> Vec<TraitAdjustment> {
177 self.correlations
178 .iter()
179 .filter(|(_, corr)| corr.abs() > 0.3) .map(|(name, corr)| {
181 let current_val = current.get_trait(name).unwrap_or(0.5);
182 TraitAdjustment {
183 trait_name: name.clone(),
184 current_value: current_val,
185 suggested_value: if *corr > 0.0 {
186 (current_val + 0.1).min(1.0) } else {
188 (current_val - 0.1).max(0.0) },
190 correlation: *corr,
191 confidence: corr.abs(),
192 }
193 })
194 .collect()
195 }
196
197 fn maybe_evict(&mut self) {
202 if self.experiments.len() > self.max_entries {
203 let initial_len = self.experiments.len();
204
205 let mut sorted: Vec<_> = self.experiments.drain(..).collect();
207
208 sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
210
211 sorted.truncate(self.max_entries);
213
214 self.experiments = VecDeque::from(sorted);
216
217 let evicted_count = initial_len - self.experiments.len();
219 metrics::counter!("vex_evolution_evictions_total").increment(evicted_count as u64);
220 }
221
222 metrics::gauge!("vex_evolution_memory_size").set(self.experiments.len() as f64);
223 }
224
225 pub fn apply_decay(&mut self, decay_factor: f64) {
227 for (_, importance) in &mut self.experiments {
228 *importance *= decay_factor;
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct TraitAdjustment {
236 pub trait_name: String,
238 pub current_value: f64,
240 pub suggested_value: f64,
242 pub correlation: f64,
244 pub confidence: f64,
246}
247
248fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
254 if x.len() != y.len() || x.is_empty() {
255 return 0.0;
256 }
257
258 if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
260 return 0.0;
261 }
262
263 let n = x.len() as f64;
264 let sum_x: f64 = x.iter().sum();
265 let sum_y: f64 = y.iter().sum();
266 let sum_xy: f64 = x.iter().zip(y).map(|(a, b)| a * b).sum();
267 let sum_x2: f64 = x.iter().map(|a| a * a).sum();
268 let sum_y2: f64 = y.iter().map(|b| b * b).sum();
269
270 if !sum_xy.is_finite() || !sum_x2.is_finite() || !sum_y2.is_finite() {
272 return 0.0;
273 }
274
275 let numerator = n * sum_xy - sum_x * sum_y;
276 let denominator = ((n * sum_x2 - sum_x.powi(2)) * (n * sum_y2 - sum_y.powi(2))).sqrt();
277
278 if denominator.abs() < 1e-10 || !numerator.is_finite() || !denominator.is_finite() {
280 return 0.0;
281 }
282
283 let result = numerator / denominator;
284
285 if !result.is_finite() {
287 0.0
288 } else {
289 result.clamp(-1.0, 1.0)
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_evolution_memory_basic() {
299 let mut memory = EvolutionMemory::new();
300
301 let genome = Genome::new("Test");
302 let exp = GenomeExperiment::new(&genome, HashMap::new(), 0.8, "Task 1");
303 memory.record(exp);
304
305 assert_eq!(memory.len(), 1);
306 assert!(!memory.is_empty());
307 }
308
309 #[test]
310 fn test_pearson_correlation() {
311 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
313 let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
314 let corr = pearson_correlation(&x, &y);
315 assert!((corr - 1.0).abs() < 0.001, "Expected ~1.0, got {}", corr);
316
317 let y_neg = vec![5.0, 4.0, 3.0, 2.0, 1.0];
319 let corr_neg = pearson_correlation(&x, &y_neg);
320 assert!(
321 (corr_neg + 1.0).abs() < 0.001,
322 "Expected ~-1.0, got {}",
323 corr_neg
324 );
325
326 let y_rand = vec![3.0, 1.0, 4.0, 2.0, 5.0];
328 let corr_rand = pearson_correlation(&x, &y_rand);
329 assert!(
330 corr_rand.abs() < 0.8,
331 "Expected low correlation, got {}",
332 corr_rand
333 );
334 }
335
336 #[test]
337 fn test_correlation_learning() {
338 let mut memory = EvolutionMemory::with_capacity(100);
339
340 for i in 0..20 {
342 let exploration = 0.3 + (i as f64 * 0.03);
343 let fitness = 0.4 + (i as f64 * 0.02);
344
345 let exp = GenomeExperiment::from_raw(
346 vec![exploration, 0.5, 0.5, 0.5, 0.5],
347 vec![
348 "exploration".into(),
349 "precision".into(),
350 "creativity".into(),
351 "skepticism".into(),
352 "verbosity".into(),
353 ],
354 fitness,
355 "test task",
356 );
357 memory.record(exp);
358 }
359
360 memory.update_correlations();
362
363 let corr = memory
365 .correlations()
366 .get("exploration")
367 .copied()
368 .unwrap_or(0.0);
369 assert!(corr > 0.5, "Expected positive correlation, got {}", corr);
370 }
371
372 #[test]
373 fn test_eviction() {
374 let mut memory = EvolutionMemory::with_capacity(5);
375
376 let genome = Genome::new("Test");
377 for i in 0..10 {
378 let exp = GenomeExperiment::new(
379 &genome,
380 HashMap::new(),
381 i as f64 / 10.0,
382 &format!("Task {}", i),
383 );
384 memory.record(exp);
385 }
386
387 assert_eq!(memory.len(), 5);
388 }
389
390 #[test]
391 fn test_suggest_adjustments() {
392 let mut memory = EvolutionMemory::new();
393
394 for i in 0..15 {
396 let exploration = 0.3 + (i as f64 * 0.04);
397 let fitness = 0.4 + (i as f64 * 0.03);
398
399 let exp = GenomeExperiment::from_raw(
400 vec![exploration, 0.5, 0.5, 0.5, 0.5],
401 vec![
402 "exploration".into(),
403 "precision".into(),
404 "creativity".into(),
405 "skepticism".into(),
406 "verbosity".into(),
407 ],
408 fitness,
409 "test",
410 );
411 memory.record(exp);
412 }
413
414 memory.update_correlations();
415
416 let genome = Genome::new("Current");
417 let suggestions = memory.suggest_adjustments(&genome);
418
419 let exp_suggestion = suggestions.iter().find(|s| s.trait_name == "exploration");
421 assert!(
422 exp_suggestion.is_some(),
423 "Should suggest exploration adjustment"
424 );
425 if let Some(s) = exp_suggestion {
426 assert!(
427 s.suggested_value > s.current_value,
428 "Should suggest increasing exploration"
429 );
430 }
431 }
432
433 #[test]
436 fn test_dos_memory_bounded() {
437 let mut memory = EvolutionMemory::new();
438
439 for i in 0..10_000 {
441 let exp = GenomeExperiment::from_raw(
442 vec![0.5; 5],
443 vec![
444 "t1".into(),
445 "t2".into(),
446 "t3".into(),
447 "t4".into(),
448 "t5".into(),
449 ],
450 0.1, &format!("spam_{}", i),
452 );
453 memory.record(exp);
454 }
455
456 assert!(
458 memory.len() <= 500,
459 "Memory grew unbounded: {} entries",
460 memory.len()
461 );
462
463 let top = memory.get_top_experiments(10);
465 assert!(
466 top.iter().all(|e| e.overall_fitness >= 0.1),
467 "Lost high-fitness experiments"
468 );
469 }
470
471 #[test]
472 fn test_pearson_nan_safety() {
473 let x = vec![f64::NAN, 1.0, 2.0];
475 let y = vec![1.0, 2.0, 3.0];
476 let result = pearson_correlation(&x, &y);
477 assert!(result.is_finite(), "Must handle NaN input, got {}", result);
478 assert_eq!(result, 0.0);
479
480 let x_inf = vec![f64::INFINITY, 1.0, 2.0];
482 let result_inf = pearson_correlation(&x_inf, &y);
483 assert!(result_inf.is_finite(), "Must handle Infinity input");
484 assert_eq!(result_inf, 0.0);
485
486 let x_big = vec![f64::MAX / 2.0; 5];
488 let y_big = vec![f64::MAX / 2.0; 5];
489 let result_big = pearson_correlation(&x_big, &y_big);
490 assert!(result_big.is_finite(), "Must handle overflow");
491 }
492
493 #[test]
494 fn test_capacity_limit() {
495 let memory = EvolutionMemory::with_capacity(1_000_000);
497
498 assert!(
500 memory.max_entries <= 10_000,
501 "Capacity not capped: {}",
502 memory.max_entries
503 );
504 }
505}