Skip to main content

oxihuman_morph/
mutation_engine.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8/// A parameter map: name → value.
9pub type ParamMap = HashMap<String, f32>;
10
11/// Defines allowed ranges and mutation behavior for one parameter.
12pub struct ParamSpec {
13    pub name: String,
14    pub min: f32,
15    pub max: f32,
16    pub step: f32,     // quantization step (0 = continuous)
17    pub mutable: bool, // can this be mutated?
18}
19
20/// The mutation configuration.
21pub struct MutationConfig {
22    pub mutation_rate: f32,         // probability each param mutates [0,1]
23    pub mutation_scale: f32,        // std dev of Gaussian mutation as fraction of range
24    pub clamp_to_range: bool,       // clamp output to [min, max]
25    pub preserve_proportions: bool, // scale all mutated params proportionally
26}
27
28/// Result of one generation step.
29pub struct MutationResult {
30    pub params: ParamMap,
31    pub mutated_keys: Vec<String>,
32    pub mutation_deltas: HashMap<String, f32>,
33}
34
35// ── LCG helpers (no rand crate) ─────────────────────────────────────────────
36
37fn 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    // Box-Muller transform
50    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
55// ── MutationEngine ───────────────────────────────────────────────────────────
56
57pub 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    /// LCG-based deterministic mutation.
68    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        // Preserve proportions: scale all mutated params so their sum stays the same.
105        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                    // Update deltas after proportion adjustment
130                    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    /// Uniform crossover: for each param, pick from parent_a or parent_b with 50/50 probability.
144    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    /// Blend crossover: interpolate each param as `a + t*(b-a)`, clamp to range.
160    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    /// Generate random params uniformly in [min, max] per spec.
172    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
189// ── Free functions ───────────────────────────────────────────────────────────
190
191/// Rank population by sum of squared normalized differences to `target`.
192/// Returns indices sorted best (smallest error) first.
193pub 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/// Tournament selection: pick the best of k random candidates.
217#[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        // Lower fitness score = better (as used in fitness_rank)
244        if fitness[idx] < fitness[best_idx] {
245            best_idx = idx;
246        }
247    }
248
249    &population[best_idx]
250}
251
252/// Return 10 human body parameter specs, all in [0, 1].
253pub 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// ── Tests ────────────────────────────────────────────────────────────────────
279
280#[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    // ── 1. new ───────────────────────────────────────────────────────────────
328    #[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    // ── 2. mutate: rate=0 produces no mutations ──────────────────────────────
336    #[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(&params, 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    // ── 3. mutate: rate=1 mutates all mutable params ─────────────────────────
348    #[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(&params, 7);
353        // "c" is not mutable, so only "a" and "b" can be mutated
354        for key in &result.mutated_keys {
355            assert_ne!(key, "c", "immutable param 'c' must not be mutated");
356        }
357        // With rate=1, both mutable params should appear in mutated_keys
358        assert!(result.mutated_keys.contains(&"a".to_string()));
359        assert!(result.mutated_keys.contains(&"b".to_string()));
360    }
361
362    // ── 4. mutate: deterministic with same seed ───────────────────────────────
363    #[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(&params, 12345);
368        let r2 = engine.mutate(&params, 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    // ── 5. mutate: different seeds give different results ────────────────────
375    #[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(&params, 1);
380        let r2 = engine.mutate(&params, 9999999);
381        // Very unlikely to be identical
382        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    // ── 6. mutate: clamping keeps values in range ────────────────────────────
387    #[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, // extreme mutation
399            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(&params, 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    // ── 7. mutate: quantization step ─────────────────────────────────────────
413    #[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(&params, 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    // ── 8. mutate: mutation_deltas match actual change ────────────────────────
443    #[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(&params, 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    // ── 9. crossover: output contains values from one of the parents ─────────
456    #[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    // ── 10. crossover: deterministic ─────────────────────────────────────────
482    #[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    // ── 11. blend_crossover: t=0 returns parent_a ────────────────────────────
495    #[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    // ── 12. blend_crossover: t=1 returns parent_b ────────────────────────────
508    #[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    // ── 13. blend_crossover: t=0.5 is midpoint ───────────────────────────────
521    #[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    // ── 14. blend_crossover: clamps to range ─────────────────────────────────
538    #[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); // t=2 would exceed range
550        assert!(child["a"] <= 1.0);
551        assert!(child["b"] <= 2.0);
552    }
553
554    // ── 15. generate_random: values in [min, max] ────────────────────────────
555    #[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    // ── 16. generate_random: all spec keys present ───────────────────────────
573    #[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    // ── 17. fitness_rank: perfect match ranks first ───────────────────────────
584    #[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        // index 1 (exact match) should rank first
604        assert_eq!(ranked[0], 1);
605    }
606
607    // ── 18. tournament_select: returns element from population ───────────────
608    #[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        // The result must be one of the population members
616        let found = pop.iter().any(|pm| pm == selected);
617        assert!(found, "selected member not found in population");
618    }
619
620    // ── 19. tournament_select: picks lowest fitness ───────────────────────────
621    #[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        // fitness[0] is the absolute best
627        let fitness: Vec<f32> = (0..10).map(|i| i as f32 * 10.0).collect();
628        // With k = population size, best must always win
629        let selected = tournament_select(&pop, &fitness, 10, 77);
630        assert_eq!(selected, &pop[0]);
631    }
632
633    // ── 20. default_human_specs: 10 specs, all [0,1] ─────────────────────────
634    #[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    // ── 21. lcg_f32: values in [0, 1) ─────────────────────────────────────────
646    #[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    // ── 22. lcg_normal: reasonable distribution ───────────────────────────────
656    #[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}