1#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8pub type ParamMap = HashMap<String, f32>;
10
11pub struct ParamSpec {
13 pub name: String,
14 pub min: f32,
15 pub max: f32,
16 pub step: f32, pub mutable: bool, }
19
20pub struct MutationConfig {
22 pub mutation_rate: f32, pub mutation_scale: f32, pub clamp_to_range: bool, pub preserve_proportions: bool, }
27
28pub struct MutationResult {
30 pub params: ParamMap,
31 pub mutated_keys: Vec<String>,
32 pub mutation_deltas: HashMap<String, f32>,
33}
34
35fn lcg_step(state: u64) -> u64 {
38 state
39 .wrapping_mul(6_364_136_223_846_793_005)
40 .wrapping_add(1_442_695_040_888_963_407)
41}
42
43fn lcg_f32(state: &mut u64) -> f32 {
44 *state = lcg_step(*state);
45 (*state >> 33) as f32 / (u32::MAX as f32)
46}
47
48fn lcg_normal(state: &mut u64) -> f32 {
49 let u1 = lcg_f32(state).max(1e-10);
51 let u2 = lcg_f32(state);
52 (-2.0_f32 * u1.ln()).sqrt() * (2.0_f32 * std::f32::consts::PI * u2).cos()
53}
54
55pub struct MutationEngine {
58 specs: Vec<ParamSpec>,
59 config: MutationConfig,
60}
61
62impl MutationEngine {
63 pub fn new(specs: Vec<ParamSpec>, config: MutationConfig) -> Self {
64 Self { specs, config }
65 }
66
67 pub fn mutate(&self, params: &ParamMap, seed: u64) -> MutationResult {
69 let mut state = seed;
70 let mut out = params.clone();
71 let mut mutated_keys = Vec::new();
72 let mut mutation_deltas: HashMap<String, f32> = HashMap::new();
73
74 for spec in &self.specs {
75 if !spec.mutable {
76 continue;
77 }
78 let roll = lcg_f32(&mut state);
79 if roll >= self.config.mutation_rate {
80 continue;
81 }
82 let range = spec.max - spec.min;
83 let noise = lcg_normal(&mut state) * self.config.mutation_scale * range;
84 let base = *out.get(&spec.name).unwrap_or(&0.0);
85 let mut new_val = base + noise;
86
87 if self.config.clamp_to_range {
88 new_val = new_val.clamp(spec.min, spec.max);
89 }
90
91 if spec.step > 0.0 {
92 new_val = (new_val / spec.step).round() * spec.step;
93 if self.config.clamp_to_range {
94 new_val = new_val.clamp(spec.min, spec.max);
95 }
96 }
97
98 let delta = new_val - base;
99 out.insert(spec.name.clone(), new_val);
100 mutated_keys.push(spec.name.clone());
101 mutation_deltas.insert(spec.name.clone(), delta);
102 }
103
104 if self.config.preserve_proportions && !mutated_keys.is_empty() {
106 let old_sum: f32 = mutated_keys
107 .iter()
108 .map(|k| params.get(k).copied().unwrap_or(0.0))
109 .sum();
110 let new_sum: f32 = mutated_keys
111 .iter()
112 .map(|k| out.get(k).copied().unwrap_or(0.0))
113 .sum();
114 if new_sum.abs() > 1e-9 {
115 let scale = old_sum / new_sum;
116 for key in &mutated_keys {
117 let v = out.get(key).copied().unwrap_or(0.0) * scale;
118 let spec = self.specs.iter().find(|s| &s.name == key);
119 let v = if self.config.clamp_to_range {
120 if let Some(s) = spec {
121 v.clamp(s.min, s.max)
122 } else {
123 v
124 }
125 } else {
126 v
127 };
128 out.insert(key.clone(), v);
129 let base = params.get(key).copied().unwrap_or(0.0);
131 mutation_deltas.insert(key.clone(), v - base);
132 }
133 }
134 }
135
136 MutationResult {
137 params: out,
138 mutated_keys,
139 mutation_deltas,
140 }
141 }
142
143 pub fn crossover(&self, parent_a: &ParamMap, parent_b: &ParamMap, seed: u64) -> ParamMap {
145 let mut state = seed;
146 let mut out = ParamMap::new();
147 for spec in &self.specs {
148 let roll = lcg_f32(&mut state);
149 let val = if roll < 0.5 {
150 parent_a.get(&spec.name).copied().unwrap_or(spec.min)
151 } else {
152 parent_b.get(&spec.name).copied().unwrap_or(spec.min)
153 };
154 out.insert(spec.name.clone(), val);
155 }
156 out
157 }
158
159 pub fn blend_crossover(&self, parent_a: &ParamMap, parent_b: &ParamMap, t: f32) -> ParamMap {
161 let mut out = ParamMap::new();
162 for spec in &self.specs {
163 let a = parent_a.get(&spec.name).copied().unwrap_or(spec.min);
164 let b = parent_b.get(&spec.name).copied().unwrap_or(spec.min);
165 let v = (a + t * (b - a)).clamp(spec.min, spec.max);
166 out.insert(spec.name.clone(), v);
167 }
168 out
169 }
170
171 pub fn generate_random(&self, seed: u64) -> ParamMap {
173 let mut state = seed;
174 let mut out = ParamMap::new();
175 for spec in &self.specs {
176 let v = spec.min + lcg_f32(&mut state) * (spec.max - spec.min);
177 let v = if spec.step > 0.0 {
178 (v / spec.step).round() * spec.step
179 } else {
180 v
181 };
182 let v = v.clamp(spec.min, spec.max);
183 out.insert(spec.name.clone(), v);
184 }
185 out
186 }
187}
188
189pub fn fitness_rank(population: &[ParamMap], target: &ParamMap, specs: &[ParamSpec]) -> Vec<usize> {
194 let mut scores: Vec<(usize, f32)> = population
195 .iter()
196 .enumerate()
197 .map(|(i, pm)| {
198 let score: f32 = specs
199 .iter()
200 .map(|spec| {
201 let range = (spec.max - spec.min).max(1e-9);
202 let v = pm.get(&spec.name).copied().unwrap_or(0.0);
203 let t = target.get(&spec.name).copied().unwrap_or(0.0);
204 let diff = (v - t) / range;
205 diff * diff
206 })
207 .sum();
208 (i, score)
209 })
210 .collect();
211
212 scores.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
213 scores.into_iter().map(|(i, _)| i).collect()
214}
215
216#[allow(clippy::too_many_arguments)]
218pub fn tournament_select<'a>(
219 population: &'a [ParamMap],
220 fitness: &[f32],
221 k: usize,
222 seed: u64,
223) -> &'a ParamMap {
224 assert!(!population.is_empty(), "population must not be empty");
225 assert_eq!(
226 population.len(),
227 fitness.len(),
228 "fitness length must match population"
229 );
230 let k = k.min(population.len()).max(1);
231
232 let mut state = seed;
233 let n = population.len();
234
235 let mut best_idx = {
236 let r = lcg_f32(&mut state);
237 (r * n as f32).floor() as usize % n
238 };
239
240 for _ in 1..k {
241 let r = lcg_f32(&mut state);
242 let idx = (r * n as f32).floor() as usize % n;
243 if fitness[idx] < fitness[best_idx] {
245 best_idx = idx;
246 }
247 }
248
249 &population[best_idx]
250}
251
252pub fn default_human_specs() -> Vec<ParamSpec> {
254 let names = [
255 "height",
256 "weight",
257 "muscle",
258 "age",
259 "head_size",
260 "neck_length",
261 "shoulder_width",
262 "hip_width",
263 "leg_length",
264 "arm_length",
265 ];
266 names
267 .iter()
268 .map(|&name| ParamSpec {
269 name: name.to_string(),
270 min: 0.0,
271 max: 1.0,
272 step: 0.0,
273 mutable: true,
274 })
275 .collect()
276}
277
278#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn simple_specs() -> Vec<ParamSpec> {
285 vec![
286 ParamSpec {
287 name: "a".into(),
288 min: 0.0,
289 max: 1.0,
290 step: 0.0,
291 mutable: true,
292 },
293 ParamSpec {
294 name: "b".into(),
295 min: 0.0,
296 max: 2.0,
297 step: 0.0,
298 mutable: true,
299 },
300 ParamSpec {
301 name: "c".into(),
302 min: -1.0,
303 max: 1.0,
304 step: 0.0,
305 mutable: false,
306 },
307 ]
308 }
309
310 fn simple_config(rate: f32) -> MutationConfig {
311 MutationConfig {
312 mutation_rate: rate,
313 mutation_scale: 0.1,
314 clamp_to_range: true,
315 preserve_proportions: false,
316 }
317 }
318
319 fn simple_params() -> ParamMap {
320 let mut m = ParamMap::new();
321 m.insert("a".into(), 0.5);
322 m.insert("b".into(), 1.0);
323 m.insert("c".into(), 0.0);
324 m
325 }
326
327 #[test]
329 fn test_new_stores_specs_and_config() {
330 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
331 assert_eq!(engine.specs.len(), 3);
332 assert!((engine.config.mutation_rate - 0.5).abs() < 1e-6);
333 }
334
335 #[test]
337 fn test_mutate_rate_zero_no_mutations() {
338 let engine = MutationEngine::new(simple_specs(), simple_config(0.0));
339 let params = simple_params();
340 let result = engine.mutate(¶ms, 42);
341 assert!(result.mutated_keys.is_empty());
342 assert!(result.mutation_deltas.is_empty());
343 assert_eq!(result.params["a"], 0.5);
344 assert_eq!(result.params["b"], 1.0);
345 }
346
347 #[test]
349 fn test_mutate_rate_one_mutates_all_mutable() {
350 let engine = MutationEngine::new(simple_specs(), simple_config(1.0));
351 let params = simple_params();
352 let result = engine.mutate(¶ms, 7);
353 for key in &result.mutated_keys {
355 assert_ne!(key, "c", "immutable param 'c' must not be mutated");
356 }
357 assert!(result.mutated_keys.contains(&"a".to_string()));
359 assert!(result.mutated_keys.contains(&"b".to_string()));
360 }
361
362 #[test]
364 fn test_mutate_deterministic() {
365 let engine = MutationEngine::new(simple_specs(), simple_config(1.0));
366 let params = simple_params();
367 let r1 = engine.mutate(¶ms, 12345);
368 let r2 = engine.mutate(¶ms, 12345);
369 assert_eq!(r1.params["a"], r2.params["a"]);
370 assert_eq!(r1.params["b"], r2.params["b"]);
371 assert_eq!(r1.mutated_keys, r2.mutated_keys);
372 }
373
374 #[test]
376 fn test_mutate_different_seeds() {
377 let engine = MutationEngine::new(simple_specs(), simple_config(1.0));
378 let params = simple_params();
379 let r1 = engine.mutate(¶ms, 1);
380 let r2 = engine.mutate(¶ms, 9999999);
381 let same = r1.params["a"] == r2.params["a"] && r1.params["b"] == r2.params["b"];
383 assert!(!same, "different seeds should produce different mutations");
384 }
385
386 #[test]
388 fn test_mutate_clamp_to_range() {
389 let specs = vec![ParamSpec {
390 name: "x".into(),
391 min: 0.0,
392 max: 1.0,
393 step: 0.0,
394 mutable: true,
395 }];
396 let config = MutationConfig {
397 mutation_rate: 1.0,
398 mutation_scale: 10.0, clamp_to_range: true,
400 preserve_proportions: false,
401 };
402 let engine = MutationEngine::new(specs, config);
403 let mut params = ParamMap::new();
404 params.insert("x".into(), 0.5);
405 for seed in 0u64..50 {
406 let result = engine.mutate(¶ms, seed);
407 let v = result.params["x"];
408 assert!((0.0..=1.0).contains(&v), "value {v} out of [0,1]");
409 }
410 }
411
412 #[test]
414 fn test_mutate_step_quantization() {
415 let specs = vec![ParamSpec {
416 name: "q".into(),
417 min: 0.0,
418 max: 1.0,
419 step: 0.25,
420 mutable: true,
421 }];
422 let config = MutationConfig {
423 mutation_rate: 1.0,
424 mutation_scale: 0.3,
425 clamp_to_range: true,
426 preserve_proportions: false,
427 };
428 let engine = MutationEngine::new(specs, config);
429 let mut params = ParamMap::new();
430 params.insert("q".into(), 0.5);
431 for seed in 0u64..30 {
432 let result = engine.mutate(¶ms, seed);
433 let v = result.params["q"];
434 let quantized = (v / 0.25).round() * 0.25;
435 assert!(
436 (v - quantized).abs() < 1e-5,
437 "value {v} not quantized to 0.25"
438 );
439 }
440 }
441
442 #[test]
444 fn test_mutate_deltas_correct() {
445 let engine = MutationEngine::new(simple_specs(), simple_config(1.0));
446 let params = simple_params();
447 let result = engine.mutate(¶ms, 42);
448 for (key, delta) in &result.mutation_deltas {
449 let original = params.get(key).copied().unwrap_or(0.0);
450 let new_val = result.params.get(key).copied().unwrap_or(0.0);
451 assert!((delta - (new_val - original)).abs() < 1e-5);
452 }
453 }
454
455 #[test]
457 fn test_crossover_values_from_parents() {
458 let specs = simple_specs();
459 let engine = MutationEngine::new(specs, simple_config(0.5));
460 let mut pa = ParamMap::new();
461 let mut pb = ParamMap::new();
462 pa.insert("a".into(), 0.1);
463 pa.insert("b".into(), 0.2);
464 pa.insert("c".into(), -0.5);
465 pb.insert("a".into(), 0.9);
466 pb.insert("b".into(), 1.8);
467 pb.insert("c".into(), 0.5);
468
469 let child = engine.crossover(&pa, &pb, 99);
470 for key in ["a", "b", "c"] {
471 let v = child[key];
472 let a_val = pa[key];
473 let b_val = pb[key];
474 assert!(
475 (v - a_val).abs() < 1e-6 || (v - b_val).abs() < 1e-6,
476 "key '{key}': value {v} is neither from parent_a ({a_val}) nor parent_b ({b_val})"
477 );
478 }
479 }
480
481 #[test]
483 fn test_crossover_deterministic() {
484 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
485 let pa = simple_params();
486 let mut pb = simple_params();
487 pb.insert("a".into(), 0.9);
488 let c1 = engine.crossover(&pa, &pb, 42);
489 let c2 = engine.crossover(&pa, &pb, 42);
490 assert_eq!(c1["a"], c2["a"]);
491 assert_eq!(c1["b"], c2["b"]);
492 }
493
494 #[test]
496 fn test_blend_crossover_t0_is_a() {
497 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
498 let pa = simple_params();
499 let mut pb = simple_params();
500 pb.insert("a".into(), 0.9);
501 pb.insert("b".into(), 1.5);
502 let child = engine.blend_crossover(&pa, &pb, 0.0);
503 assert!((child["a"] - pa["a"]).abs() < 1e-5);
504 assert!((child["b"] - pa["b"]).abs() < 1e-5);
505 }
506
507 #[test]
509 fn test_blend_crossover_t1_is_b() {
510 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
511 let pa = simple_params();
512 let mut pb = simple_params();
513 pb.insert("a".into(), 0.9);
514 pb.insert("b".into(), 1.5);
515 let child = engine.blend_crossover(&pa, &pb, 1.0);
516 assert!((child["a"] - pb["a"]).abs() < 1e-5);
517 assert!((child["b"] - pb["b"]).abs() < 1e-5);
518 }
519
520 #[test]
522 fn test_blend_crossover_midpoint() {
523 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
524 let mut pa = ParamMap::new();
525 let mut pb = ParamMap::new();
526 pa.insert("a".into(), 0.0);
527 pa.insert("b".into(), 0.0);
528 pa.insert("c".into(), -1.0);
529 pb.insert("a".into(), 1.0);
530 pb.insert("b".into(), 2.0);
531 pb.insert("c".into(), 1.0);
532 let child = engine.blend_crossover(&pa, &pb, 0.5);
533 assert!((child["a"] - 0.5).abs() < 1e-5);
534 assert!((child["b"] - 1.0).abs() < 1e-5);
535 }
536
537 #[test]
539 fn test_blend_crossover_clamps() {
540 let engine = MutationEngine::new(simple_specs(), simple_config(0.5));
541 let mut pa = ParamMap::new();
542 let mut pb = ParamMap::new();
543 pa.insert("a".into(), 0.8);
544 pa.insert("b".into(), 1.0);
545 pa.insert("c".into(), 0.0);
546 pb.insert("a".into(), 1.0);
547 pb.insert("b".into(), 2.0);
548 pb.insert("c".into(), 0.0);
549 let child = engine.blend_crossover(&pa, &pb, 2.0); assert!(child["a"] <= 1.0);
551 assert!(child["b"] <= 2.0);
552 }
553
554 #[test]
556 fn test_generate_random_in_range() {
557 let engine = MutationEngine::new(default_human_specs(), simple_config(0.5));
558 for seed in 0u64..20 {
559 let pm = engine.generate_random(seed);
560 for spec in default_human_specs() {
561 let v = pm[&spec.name];
562 assert!(
563 v >= spec.min && v <= spec.max,
564 "param '{}' = {} out of range",
565 spec.name,
566 v
567 );
568 }
569 }
570 }
571
572 #[test]
574 fn test_generate_random_all_keys_present() {
575 let specs = default_human_specs();
576 let engine = MutationEngine::new(specs, simple_config(0.5));
577 let pm = engine.generate_random(123);
578 for spec in default_human_specs() {
579 assert!(pm.contains_key(&spec.name), "missing key '{}'", spec.name);
580 }
581 }
582
583 #[test]
585 fn test_fitness_rank_perfect_match_first() {
586 let specs = default_human_specs();
587 let mut target = ParamMap::new();
588 for s in &specs {
589 target.insert(s.name.clone(), 0.5);
590 }
591
592 let mut exact = ParamMap::new();
593 for s in &specs {
594 exact.insert(s.name.clone(), 0.5);
595 }
596 let mut far = ParamMap::new();
597 for s in &specs {
598 far.insert(s.name.clone(), 1.0);
599 }
600
601 let population = vec![far, exact];
602 let ranked = fitness_rank(&population, &target, &specs);
603 assert_eq!(ranked[0], 1);
605 }
606
607 #[test]
609 fn test_tournament_select_returns_population_member() {
610 let specs = default_human_specs();
611 let engine = MutationEngine::new(specs, simple_config(0.5));
612 let pop: Vec<ParamMap> = (0..5).map(|s| engine.generate_random(s)).collect();
613 let fitness: Vec<f32> = (0..5).map(|i| i as f32).collect();
614 let selected = tournament_select(&pop, &fitness, 3, 42);
615 let found = pop.iter().any(|pm| pm == selected);
617 assert!(found, "selected member not found in population");
618 }
619
620 #[test]
622 fn test_tournament_select_prefers_best_fitness() {
623 let specs = default_human_specs();
624 let engine = MutationEngine::new(specs, simple_config(0.5));
625 let pop: Vec<ParamMap> = (0..10).map(|s| engine.generate_random(s)).collect();
626 let fitness: Vec<f32> = (0..10).map(|i| i as f32 * 10.0).collect();
628 let selected = tournament_select(&pop, &fitness, 10, 77);
630 assert_eq!(selected, &pop[0]);
631 }
632
633 #[test]
635 fn test_default_human_specs_count_and_range() {
636 let specs = default_human_specs();
637 assert_eq!(specs.len(), 10);
638 for spec in &specs {
639 assert!((spec.min - 0.0).abs() < 1e-6);
640 assert!((spec.max - 1.0).abs() < 1e-6);
641 assert!(spec.mutable);
642 }
643 }
644
645 #[test]
647 fn test_lcg_f32_range() {
648 let mut state = 123456789u64;
649 for _ in 0..1000 {
650 let v = lcg_f32(&mut state);
651 assert!((0.0..1.0 + 1e-5).contains(&v), "lcg_f32 out of range: {v}");
652 }
653 }
654
655 #[test]
657 fn test_lcg_normal_distribution() {
658 let mut state = 42u64;
659 let samples: Vec<f32> = (0..1000).map(|_| lcg_normal(&mut state)).collect();
660 let mean = samples.iter().sum::<f32>() / samples.len() as f32;
661 let var = samples.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / samples.len() as f32;
662 assert!(mean.abs() < 0.2, "mean {mean} too far from 0");
663 assert!((var - 1.0).abs() < 1.0, "variance {var} too far from 1");
664 }
665}