sc_neurocore_engine/
evo.rs1use rayon::prelude::*;
17
18struct Xoshiro256pp([u64; 4]);
21
22impl Xoshiro256pp {
23 fn new(seed: u64) -> Self {
24 let mut s = [0u64; 4];
25 let mut z = seed.wrapping_add(0x9E3779B97F4A7C15);
26 for slot in s.iter_mut() {
27 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
28 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
29 *slot = z ^ (z >> 31);
30 }
31 Self(s)
32 }
33
34 fn next_u64(&mut self) -> u64 {
35 let result = (self.0[0].wrapping_add(self.0[3]))
36 .rotate_left(23)
37 .wrapping_add(self.0[0]);
38 let t = self.0[1] << 17;
39 self.0[2] ^= self.0[0];
40 self.0[3] ^= self.0[1];
41 self.0[1] ^= self.0[2];
42 self.0[0] ^= self.0[3];
43 self.0[2] ^= t;
44 self.0[3] = self.0[3].rotate_left(45);
45 result
46 }
47
48 fn next_f64(&mut self) -> f64 {
49 (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
50 }
51
52 fn next_gaussian(&mut self) -> f64 {
53 let u1 = self.next_f64().max(1e-15);
55 let u2 = self.next_f64();
56 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
57 }
58}
59
60pub fn batch_mutate_weights(
66 genomes: &mut [Vec<f64>],
67 mutation_rate: f64,
68 mutation_scale: f64,
69 seed: u64,
70) {
71 genomes.par_iter_mut().enumerate().for_each(|(i, genome)| {
72 let mut rng = Xoshiro256pp::new(seed.wrapping_add(i as u64));
73 for w in genome.iter_mut() {
74 if rng.next_f64() < mutation_rate {
75 *w += rng.next_gaussian() * mutation_scale;
76 *w = w.clamp(-10.0, 10.0);
77 }
78 }
79 });
80}
81
82pub fn batch_evaluate_fitness(genomes: &[Vec<f64>], inputs: &[f64], target: f64) -> Vec<f64> {
89 genomes
90 .par_iter()
91 .map(|genome| {
92 let output: f64 = genome.iter().zip(inputs.iter()).map(|(w, x)| w * x).sum();
93 let error = output - target;
94 -(error * error) })
96 .collect()
97}
98
99pub fn batch_crossover(parents_a: &[Vec<f64>], parents_b: &[Vec<f64>], seed: u64) -> Vec<Vec<f64>> {
103 parents_a
104 .par_iter()
105 .zip(parents_b.par_iter())
106 .enumerate()
107 .map(|(i, (a, b))| {
108 let mut rng = Xoshiro256pp::new(seed.wrapping_add(i as u64));
109 let len = a.len().min(b.len());
110 let mut child = Vec::with_capacity(len);
111 for j in 0..len {
112 if rng.next_f64() < 0.5 {
113 child.push(a[j]);
114 } else {
115 child.push(b[j]);
116 }
117 }
118 child
119 })
120 .collect()
121}
122
123pub fn population_diversity(genomes: &[Vec<f64>]) -> f64 {
127 let n = genomes.len();
128 if n < 2 {
129 return 0.0;
130 }
131
132 let total: f64 = (0..n)
133 .into_par_iter()
134 .flat_map(|i| ((i + 1)..n).into_par_iter().map(move |j| (i, j)))
135 .map(|(i, j)| {
136 let len = genomes[i].len().min(genomes[j].len());
137 let dist_sq: f64 = (0..len)
138 .map(|k| {
139 let d = genomes[i][k] - genomes[j][k];
140 d * d
141 })
142 .sum();
143 dist_sq.sqrt()
144 })
145 .sum();
146
147 let pairs = (n * (n - 1)) / 2;
148 total / pairs as f64
149}
150
151pub fn novelty_scores(genomes: &[Vec<f64>], archive: &[Vec<f64>], k_nearest: usize) -> Vec<f64> {
153 let combined: Vec<&Vec<f64>> = archive.iter().chain(genomes.iter()).collect();
154
155 genomes
156 .par_iter()
157 .map(|genome| {
158 let mut distances: Vec<f64> = combined
159 .iter()
160 .filter(|&&other| !std::ptr::eq(other, genome))
161 .map(|other| {
162 let len = genome.len().min(other.len());
163 let d: f64 = (0..len)
164 .map(|k| {
165 let diff = genome[k] - other[k];
166 diff * diff
167 })
168 .sum();
169 d.sqrt()
170 })
171 .collect();
172 distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
173 let k = k_nearest.min(distances.len());
174 if k == 0 {
175 return 0.0;
176 }
177 distances[..k].iter().sum::<f64>() / k as f64
178 })
179 .collect()
180}
181
182pub fn tournament_select(
186 fitness: &[f64],
187 n_select: usize,
188 tournament_size: usize,
189 seed: u64,
190) -> Vec<usize> {
191 let pop_size = fitness.len();
192 let mut rng = Xoshiro256pp::new(seed);
193 let mut selected = Vec::with_capacity(n_select);
194
195 for _ in 0..n_select {
196 let mut best_idx = (rng.next_u64() % pop_size as u64) as usize;
197 let mut best_fit = fitness[best_idx];
198 for _ in 1..tournament_size {
199 let idx = (rng.next_u64() % pop_size as u64) as usize;
200 if fitness[idx] > best_fit {
201 best_idx = idx;
202 best_fit = fitness[idx];
203 }
204 }
205 selected.push(best_idx);
206 }
207
208 selected
209}
210
211#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_batch_mutate() {
219 let mut pop = vec![vec![0.0; 10]; 50];
220 batch_mutate_weights(&mut pop, 0.5, 0.1, 42);
221 let changed = pop.iter().any(|g| g.iter().any(|&w| w != 0.0));
222 assert!(changed);
223 }
224
225 #[test]
226 fn test_mutate_bounded() {
227 let mut pop = vec![vec![100.0; 5]; 10];
228 batch_mutate_weights(&mut pop, 1.0, 1.0, 42);
229 for g in &pop {
230 for &w in g {
231 assert!((-10.0..=10.0).contains(&w));
232 }
233 }
234 }
235
236 #[test]
237 fn test_batch_fitness() {
238 let genomes = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
239 let inputs = vec![1.0, 0.0];
240 let fitness = batch_evaluate_fitness(&genomes, &inputs, 1.0);
241 assert!((fitness[0] - 0.0).abs() < 1e-10); assert!(fitness[1] < 0.0); }
244
245 #[test]
246 fn test_crossover_length() {
247 let a = vec![vec![1.0; 10]; 20];
248 let b = vec![vec![2.0; 10]; 20];
249 let children = batch_crossover(&a, &b, 42);
250 assert_eq!(children.len(), 20);
251 assert_eq!(children[0].len(), 10);
252 }
253
254 #[test]
255 fn test_crossover_mix() {
256 let a = vec![vec![0.0; 100]];
257 let b = vec![vec![1.0; 100]];
258 let children = batch_crossover(&a, &b, 42);
259 let has_zero = children[0].contains(&0.0);
260 let has_one = children[0].contains(&1.0);
261 assert!(has_zero && has_one);
262 }
263
264 #[test]
265 fn test_diversity_identical() {
266 let pop = vec![vec![1.0; 5]; 10];
267 assert!((population_diversity(&pop) - 0.0).abs() < 1e-10);
268 }
269
270 #[test]
271 fn test_diversity_distinct() {
272 let pop = vec![vec![0.0; 5], vec![1.0; 5]];
273 assert!(population_diversity(&pop) > 0.0);
274 }
275
276 #[test]
277 fn test_novelty_scores() {
278 let genomes = vec![vec![0.0; 5], vec![10.0; 5]];
279 let archive = vec![vec![0.0; 5]];
280 let scores = novelty_scores(&genomes, &archive, 1);
281 assert!(scores[1] > scores[0]); }
283
284 #[test]
285 fn test_tournament_select() {
286 let fitness = vec![0.1, 0.9, 0.5, 0.3];
287 let selected = tournament_select(&fitness, 10, 3, 42);
288 assert_eq!(selected.len(), 10);
289 let count_best = selected.iter().filter(|&&i| i == 1).count();
291 assert!(count_best > 0);
292 }
293
294 #[test]
295 fn test_single_genome_diversity() {
296 let pop = vec![vec![1.0; 5]];
297 assert!((population_diversity(&pop) - 0.0).abs() < 1e-10);
298 }
299}