1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AgentGenome {
13 pub sense_radius: f64,
15 pub max_idle: u64,
17 pub keyword_boost: f64,
19 pub explore_bias: f64,
21 pub boundary_bias: f64,
23
24 pub tentative_weight: f64,
27 pub reinforcement_boost: f64,
29 pub wiring_selectivity: f64,
32}
33
34impl AgentGenome {
35 pub fn default_genome() -> Self {
37 Self {
38 sense_radius: 10.0,
39 max_idle: 30,
40 keyword_boost: 3.0,
41 explore_bias: 0.2,
42 boundary_bias: 0.0,
43 tentative_weight: 0.1,
44 reinforcement_boost: 0.1,
45 wiring_selectivity: 1.0,
46 }
47 }
48
49 pub fn mutate(&self, mutation_rate: f64, seed: u64) -> Self {
54 let mut rng = seed;
55 let mut next = || -> f64 {
56 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
57 ((rng >> 33) as f64 / (u32::MAX as f64)) * 2.0 - 1.0
59 };
60
61 Self {
62 sense_radius: (self.sense_radius * (1.0 + next() * mutation_rate)).clamp(2.0, 30.0),
63 max_idle: ((self.max_idle as f64 * (1.0 + next() * mutation_rate)).round() as u64).clamp(5, 100),
64 keyword_boost: (self.keyword_boost * (1.0 + next() * mutation_rate)).clamp(0.5, 10.0),
65 explore_bias: (self.explore_bias + next() * mutation_rate * 0.5).clamp(0.0, 1.0),
66 boundary_bias: (self.boundary_bias + next() * mutation_rate * 0.5).clamp(-1.0, 1.0),
67 tentative_weight: (self.tentative_weight * (1.0 + next() * mutation_rate)).clamp(0.05, 0.5),
68 reinforcement_boost: (self.reinforcement_boost * (1.0 + next() * mutation_rate)).clamp(0.01, 0.3),
69 wiring_selectivity: (self.wiring_selectivity + next() * mutation_rate * 0.3).clamp(0.1, 1.0),
70 }
71 }
72
73 pub fn distance(&self, other: &AgentGenome) -> f64 {
76 let dims = [
77 ((self.sense_radius - 2.0) / 28.0, (other.sense_radius - 2.0) / 28.0),
78 (self.max_idle as f64 / 100.0, other.max_idle as f64 / 100.0),
79 ((self.keyword_boost - 0.5) / 9.5, (other.keyword_boost - 0.5) / 9.5),
80 (self.explore_bias, other.explore_bias),
81 ((self.boundary_bias + 1.0) / 2.0, (other.boundary_bias + 1.0) / 2.0),
82 ((self.tentative_weight - 0.05) / 0.45, (other.tentative_weight - 0.05) / 0.45),
83 ((self.reinforcement_boost - 0.01) / 0.29, (other.reinforcement_boost - 0.01) / 0.29),
84 ((self.wiring_selectivity - 0.1) / 0.9, (other.wiring_selectivity - 0.1) / 0.9),
85 ];
86
87 let sum_sq: f64 = dims.iter().map(|(a, b)| (a - b).powi(2)).sum();
88 sum_sq.sqrt()
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn default_genome_is_valid() {
98 let g = AgentGenome::default_genome();
99 assert!(g.sense_radius > 0.0);
100 assert!(g.max_idle > 0);
101 }
102
103 #[test]
104 fn mutation_changes_genome() {
105 let g = AgentGenome::default_genome();
106 let mutated = g.mutate(0.2, 42);
107 let same = (g.sense_radius - mutated.sense_radius).abs() < 1e-10
109 && g.max_idle == mutated.max_idle
110 && (g.keyword_boost - mutated.keyword_boost).abs() < 1e-10;
111 assert!(!same, "Mutation should change at least one parameter");
112 }
113
114 #[test]
115 fn mutation_stays_in_bounds() {
116 let g = AgentGenome::default_genome();
117 for seed in 0..100 {
118 let m = g.mutate(0.5, seed);
119 assert!(m.sense_radius >= 2.0 && m.sense_radius <= 30.0);
120 assert!(m.max_idle >= 5 && m.max_idle <= 100);
121 assert!(m.explore_bias >= 0.0 && m.explore_bias <= 1.0);
122 }
123 }
124
125 #[test]
126 fn distance_is_zero_for_same_genome() {
127 let g = AgentGenome::default_genome();
128 assert!(g.distance(&g) < 1e-10);
129 }
130
131 #[test]
132 fn distance_increases_with_mutation() {
133 let g = AgentGenome::default_genome();
134 let m1 = g.mutate(0.1, 42);
135 let m2 = g.mutate(0.5, 42);
136 assert!(g.distance(&m2) >= g.distance(&m1) * 0.5,
137 "Larger mutation should generally produce larger distance");
138 }
139}