use std::collections::{HashMap, VecDeque};
use crate::evolution::Genome;
use crate::genome_experiment::GenomeExperiment;
#[derive(Debug, Clone, Default)]
pub struct EvolutionMemory {
experiments: VecDeque<(GenomeExperiment, f64)>, max_entries: usize,
correlations: HashMap<String, f64>,
}
impl EvolutionMemory {
pub fn new() -> Self {
Self {
experiments: VecDeque::new(),
max_entries: 500,
correlations: HashMap::new(),
}
}
pub fn with_capacity(max_entries: usize) -> Self {
let safe_capacity = max_entries.min(10_000);
Self {
experiments: VecDeque::with_capacity(safe_capacity.min(100)),
max_entries: safe_capacity,
correlations: HashMap::new(),
}
}
pub fn record(&mut self, experiment: GenomeExperiment) {
let importance = experiment.overall_fitness;
self.experiments.push_front((experiment, importance));
metrics::counter!("vex_experiments_recorded_total").increment(1);
self.maybe_evict();
if self.experiments.len().is_multiple_of(10) {
self.update_correlations();
}
}
pub fn get_top_experiments(&self, limit: usize) -> Vec<&GenomeExperiment> {
let mut sorted: Vec<_> = self.experiments.iter().collect();
sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
sorted.into_iter().take(limit).map(|(exp, _)| exp).collect()
}
pub fn experiments(&self) -> impl Iterator<Item = &GenomeExperiment> {
self.experiments.iter().map(|(exp, _)| exp)
}
pub fn len(&self) -> usize {
self.experiments.len()
}
pub fn is_empty(&self) -> bool {
self.experiments.is_empty()
}
pub fn correlations(&self) -> &HashMap<String, f64> {
&self.correlations
}
pub fn get_experiments_snapshot(&self) -> Vec<GenomeExperiment> {
self.experiments
.iter()
.map(|(exp, _)| exp.clone())
.collect()
}
pub fn get_experiments_oldest(&self, count: usize) -> Vec<GenomeExperiment> {
self.experiments
.iter()
.take(count)
.map(|(exp, _)| exp.clone())
.collect()
}
pub fn clear(&mut self) {
self.experiments.clear();
metrics::gauge!("vex_evolution_memory_size").set(0.0);
}
pub fn drain_oldest(&mut self, count: usize) {
let actual_count = count.min(self.experiments.len());
self.experiments.drain(0..actual_count);
metrics::gauge!("vex_evolution_memory_size").set(self.experiments.len() as f64);
}
fn update_correlations(&mut self) {
if self.experiments.len() < 10 {
return;
}
let trait_count = self
.experiments
.front()
.map(|(e, _)| e.traits.len())
.unwrap_or(5);
for i in 0..trait_count {
let trait_name = self
.experiments
.front()
.and_then(|(e, _)| e.trait_names.get(i).cloned())
.unwrap_or_else(|| format!("trait_{}", i));
let trait_values: Vec<f64> = self
.experiments
.iter()
.filter_map(|(e, _)| e.traits.get(i).copied())
.collect();
let fitness_values: Vec<f64> = self
.experiments
.iter()
.map(|(e, _)| e.overall_fitness)
.collect();
if trait_values.len() >= 10 {
let corr = pearson_correlation(&trait_values, &fitness_values);
self.correlations.insert(trait_name, corr);
}
}
metrics::gauge!("vex_learned_correlations_count").set(self.correlations.len() as f64);
}
pub fn suggest_adjustments(&self, current: &Genome) -> Vec<TraitAdjustment> {
self.correlations
.iter()
.filter(|(_, corr)| corr.abs() > 0.3) .map(|(name, corr)| {
let current_val = current.get_trait(name).unwrap_or(0.5);
TraitAdjustment {
trait_name: name.clone(),
current_value: current_val,
suggested_value: if *corr > 0.0 {
(current_val + 0.1).min(1.0) } else {
(current_val - 0.1).max(0.0) },
correlation: *corr,
confidence: corr.abs(),
}
})
.collect()
}
fn maybe_evict(&mut self) {
if self.experiments.len() > self.max_entries {
let initial_len = self.experiments.len();
let mut sorted: Vec<_> = self.experiments.drain(..).collect();
sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
sorted.truncate(self.max_entries);
self.experiments = VecDeque::from(sorted);
let evicted_count = initial_len - self.experiments.len();
metrics::counter!("vex_evolution_evictions_total").increment(evicted_count as u64);
}
metrics::gauge!("vex_evolution_memory_size").set(self.experiments.len() as f64);
}
pub fn apply_decay(&mut self, decay_factor: f64) {
for (_, importance) in &mut self.experiments {
*importance *= decay_factor;
}
}
}
#[derive(Debug, Clone)]
pub struct TraitAdjustment {
pub trait_name: String,
pub current_value: f64,
pub suggested_value: f64,
pub correlation: f64,
pub confidence: f64,
}
fn pearson_correlation(x: &[f64], y: &[f64]) -> f64 {
if x.len() != y.len() || x.is_empty() {
return 0.0;
}
if x.iter().any(|v| !v.is_finite()) || y.iter().any(|v| !v.is_finite()) {
return 0.0;
}
let n = x.len() as f64;
let sum_x: f64 = x.iter().sum();
let sum_y: f64 = y.iter().sum();
let sum_xy: f64 = x.iter().zip(y).map(|(a, b)| a * b).sum();
let sum_x2: f64 = x.iter().map(|a| a * a).sum();
let sum_y2: f64 = y.iter().map(|b| b * b).sum();
if !sum_xy.is_finite() || !sum_x2.is_finite() || !sum_y2.is_finite() {
return 0.0;
}
let numerator = n * sum_xy - sum_x * sum_y;
let denominator = ((n * sum_x2 - sum_x.powi(2)) * (n * sum_y2 - sum_y.powi(2))).sqrt();
if denominator.abs() < 1e-10 || !numerator.is_finite() || !denominator.is_finite() {
return 0.0;
}
let result = numerator / denominator;
if !result.is_finite() {
0.0
} else {
result.clamp(-1.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_evolution_memory_basic() {
let mut memory = EvolutionMemory::new();
let genome = Genome::new("Test");
let exp = GenomeExperiment::new(&genome, HashMap::new(), 0.8, "Task 1");
memory.record(exp);
assert_eq!(memory.len(), 1);
assert!(!memory.is_empty());
}
#[test]
fn test_pearson_correlation() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let corr = pearson_correlation(&x, &y);
assert!((corr - 1.0).abs() < 0.001, "Expected ~1.0, got {}", corr);
let y_neg = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let corr_neg = pearson_correlation(&x, &y_neg);
assert!(
(corr_neg + 1.0).abs() < 0.001,
"Expected ~-1.0, got {}",
corr_neg
);
let y_rand = vec![3.0, 1.0, 4.0, 2.0, 5.0];
let corr_rand = pearson_correlation(&x, &y_rand);
assert!(
corr_rand.abs() < 0.8,
"Expected low correlation, got {}",
corr_rand
);
}
#[test]
fn test_correlation_learning() {
let mut memory = EvolutionMemory::with_capacity(100);
for i in 0..20 {
let exploration = 0.3 + (i as f64 * 0.03);
let fitness = 0.4 + (i as f64 * 0.02);
let exp = GenomeExperiment::from_raw(
vec![exploration, 0.5, 0.5, 0.5, 0.5],
vec![
"exploration".into(),
"precision".into(),
"creativity".into(),
"skepticism".into(),
"verbosity".into(),
],
fitness,
"test task",
);
memory.record(exp);
}
memory.update_correlations();
let corr = memory
.correlations()
.get("exploration")
.copied()
.unwrap_or(0.0);
assert!(corr > 0.5, "Expected positive correlation, got {}", corr);
}
#[test]
fn test_eviction() {
let mut memory = EvolutionMemory::with_capacity(5);
let genome = Genome::new("Test");
for i in 0..10 {
let exp = GenomeExperiment::new(
&genome,
HashMap::new(),
i as f64 / 10.0,
&format!("Task {}", i),
);
memory.record(exp);
}
assert_eq!(memory.len(), 5);
}
#[test]
fn test_suggest_adjustments() {
let mut memory = EvolutionMemory::new();
for i in 0..15 {
let exploration = 0.3 + (i as f64 * 0.04);
let fitness = 0.4 + (i as f64 * 0.03);
let exp = GenomeExperiment::from_raw(
vec![exploration, 0.5, 0.5, 0.5, 0.5],
vec![
"exploration".into(),
"precision".into(),
"creativity".into(),
"skepticism".into(),
"verbosity".into(),
],
fitness,
"test",
);
memory.record(exp);
}
memory.update_correlations();
let genome = Genome::new("Current");
let suggestions = memory.suggest_adjustments(&genome);
let exp_suggestion = suggestions.iter().find(|s| s.trait_name == "exploration");
assert!(
exp_suggestion.is_some(),
"Should suggest exploration adjustment"
);
if let Some(s) = exp_suggestion {
assert!(
s.suggested_value > s.current_value,
"Should suggest increasing exploration"
);
}
}
#[test]
fn test_dos_memory_bounded() {
let mut memory = EvolutionMemory::new();
for i in 0..10_000 {
let exp = GenomeExperiment::from_raw(
vec![0.5; 5],
vec![
"t1".into(),
"t2".into(),
"t3".into(),
"t4".into(),
"t5".into(),
],
0.1, &format!("spam_{}", i),
);
memory.record(exp);
}
assert!(
memory.len() <= 500,
"Memory grew unbounded: {} entries",
memory.len()
);
let top = memory.get_top_experiments(10);
assert!(
top.iter().all(|e| e.overall_fitness >= 0.1),
"Lost high-fitness experiments"
);
}
#[test]
fn test_pearson_nan_safety() {
let x = vec![f64::NAN, 1.0, 2.0];
let y = vec![1.0, 2.0, 3.0];
let result = pearson_correlation(&x, &y);
assert!(result.is_finite(), "Must handle NaN input, got {}", result);
assert_eq!(result, 0.0);
let x_inf = vec![f64::INFINITY, 1.0, 2.0];
let result_inf = pearson_correlation(&x_inf, &y);
assert!(result_inf.is_finite(), "Must handle Infinity input");
assert_eq!(result_inf, 0.0);
let x_big = vec![f64::MAX / 2.0; 5];
let y_big = vec![f64::MAX / 2.0; 5];
let result_big = pearson_correlation(&x_big, &y_big);
assert!(result_big.is_finite(), "Must handle overflow");
}
#[test]
fn test_capacity_limit() {
let memory = EvolutionMemory::with_capacity(1_000_000);
assert!(
memory.max_entries <= 10_000,
"Capacity not capped: {}",
memory.max_entries
);
}
}