Skip to main content

oxihuman_morph/
param_randomizer.rs

1#![allow(dead_code)]
2//! Deterministic parameter randomization for morph variation.
3
4#[allow(dead_code)]
5#[derive(Clone, Debug)]
6pub struct ParamRandomizer {
7    seed: u64,
8    min: f32,
9    max: f32,
10    count: usize,
11}
12
13#[allow(dead_code)]
14pub fn new_param_randomizer(min: f32, max: f32) -> ParamRandomizer {
15    ParamRandomizer {
16        seed: 42,
17        min,
18        max,
19        count: 0,
20    }
21}
22
23#[allow(dead_code)]
24pub fn randomize_in_range(r: &mut ParamRandomizer) -> f32 {
25    r.seed = r
26        .seed
27        .wrapping_mul(6364136223846793005)
28        .wrapping_add(1442695040888963407);
29    r.count += 1;
30    let frac = ((r.seed >> 33) as f32) / (u32::MAX as f32);
31    r.min + (r.max - r.min) * frac.clamp(0.0, 1.0)
32}
33
34/// Draw an approximate Gaussian variate via the Box-Muller transform.
35///
36/// Two uniform samples are drawn from the LCG, mapped through the Box-Muller
37/// formula to produce a standard normal `z`, then scaled and shifted to
38/// `[min, max]` by interpreting the range as `[mean - 3σ, mean + 3σ]`.
39///
40/// The result is clamped to `[r.min, r.max]` so the output always stays in
41/// range even when `z` lands in the tails.
42#[allow(dead_code)]
43pub fn randomize_gaussian_stub(r: &mut ParamRandomizer) -> f32 {
44    // Box-Muller: need two uniform samples in (0, 1].
45    let mut u1 = {
46        r.seed = r
47            .seed
48            .wrapping_mul(6364136223846793005)
49            .wrapping_add(1442695040888963407);
50        r.count += 1;
51        ((r.seed >> 33) as f32) / (u32::MAX as f32)
52    };
53    let u2 = {
54        r.seed = r
55            .seed
56            .wrapping_mul(6364136223846793005)
57            .wrapping_add(1442695040888963407);
58        r.count += 1;
59        ((r.seed >> 33) as f32) / (u32::MAX as f32)
60    };
61
62    // Guard against u1 = 0 to avoid ln(0).
63    if u1 < 1e-10 {
64        u1 = 1e-10_f32;
65    }
66
67    // Standard normal variate via Box-Muller transform.
68    let z = (-2.0_f32 * u1.ln()).sqrt() * (2.0_f32 * std::f32::consts::PI * u2).cos();
69
70    // Interpret [r.min, r.max] as [mean - 3σ, mean + 3σ]:
71    //   mean  = (min + max) / 2
72    //   std   = (max - min) / 6
73    let mean = (r.min + r.max) * 0.5_f32;
74    let std_dev = (r.max - r.min) / 6.0_f32;
75
76    (mean + z * std_dev).clamp(r.min, r.max)
77}
78
79#[allow(dead_code)]
80pub fn seed_randomizer(r: &mut ParamRandomizer, seed: u64) {
81    r.seed = seed;
82    r.count = 0;
83}
84
85#[allow(dead_code)]
86pub fn param_min(r: &ParamRandomizer) -> f32 {
87    r.min
88}
89
90#[allow(dead_code)]
91pub fn param_max(r: &ParamRandomizer) -> f32 {
92    r.max
93}
94
95#[allow(dead_code)]
96pub fn randomized_count(r: &ParamRandomizer) -> usize {
97    r.count
98}
99
100#[allow(dead_code)]
101pub fn randomizer_to_json(r: &ParamRandomizer) -> String {
102    format!(
103        "{{\"seed\":{},\"min\":{},\"max\":{},\"count\":{}}}",
104        r.seed, r.min, r.max, r.count
105    )
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_new_param_randomizer() {
114        let r = new_param_randomizer(0.0, 1.0);
115        assert!((param_min(&r)).abs() < 1e-6);
116        assert!((param_max(&r) - 1.0).abs() < 1e-6);
117    }
118
119    #[test]
120    fn test_randomize_in_range() {
121        let mut r = new_param_randomizer(0.0, 1.0);
122        let v = randomize_in_range(&mut r);
123        assert!((0.0..=1.0).contains(&v));
124    }
125
126    #[test]
127    fn test_randomize_gaussian_stub() {
128        let mut r = new_param_randomizer(0.0, 1.0);
129        let v = randomize_gaussian_stub(&mut r);
130        assert!((0.0..=1.0).contains(&v));
131    }
132
133    #[test]
134    fn test_seed_randomizer() {
135        let mut r = new_param_randomizer(0.0, 1.0);
136        seed_randomizer(&mut r, 123);
137        let v1 = randomize_in_range(&mut r);
138        seed_randomizer(&mut r, 123);
139        let v2 = randomize_in_range(&mut r);
140        assert!((v1 - v2).abs() < 1e-6);
141    }
142
143    #[test]
144    fn test_randomized_count() {
145        let mut r = new_param_randomizer(0.0, 1.0);
146        assert_eq!(randomized_count(&r), 0);
147        randomize_in_range(&mut r);
148        assert_eq!(randomized_count(&r), 1);
149    }
150
151    #[test]
152    fn test_randomizer_to_json() {
153        let r = new_param_randomizer(0.0, 1.0);
154        let json = randomizer_to_json(&r);
155        assert!(json.contains("\"seed\":"));
156    }
157
158    #[test]
159    fn test_range_min_max() {
160        let mut r = new_param_randomizer(5.0, 10.0);
161        for _ in 0..20 {
162            let v = randomize_in_range(&mut r);
163            assert!((5.0..=10.0).contains(&v));
164        }
165    }
166
167    #[test]
168    fn test_param_min() {
169        let r = new_param_randomizer(-1.0, 1.0);
170        assert!((param_min(&r) - (-1.0)).abs() < 1e-6);
171    }
172
173    #[test]
174    fn test_param_max() {
175        let r = new_param_randomizer(0.0, 2.0);
176        assert!((param_max(&r) - 2.0).abs() < 1e-6);
177    }
178
179    #[test]
180    fn test_deterministic() {
181        let mut r1 = new_param_randomizer(0.0, 1.0);
182        let mut r2 = new_param_randomizer(0.0, 1.0);
183        for _ in 0..10 {
184            assert!((randomize_in_range(&mut r1) - randomize_in_range(&mut r2)).abs() < 1e-6);
185        }
186    }
187
188    #[test]
189    fn randomize_gaussian_mean_centered() {
190        // Over 1000 draws the sample mean should converge close to the
191        // true mean of the distribution (loose tolerance for a deterministic LCG).
192        let min = 0.0_f32;
193        let max = 10.0_f32;
194        let expected_mean = (min + max) * 0.5;
195        let mut r = new_param_randomizer(min, max);
196        let n = 1000;
197        let sum: f32 = (0..n).map(|_| randomize_gaussian_stub(&mut r)).sum();
198        let sample_mean = sum / n as f32;
199        assert!(
200            (sample_mean - expected_mean).abs() < 0.5,
201            "sample mean {sample_mean:.4} not within 0.5 of expected {expected_mean}"
202        );
203    }
204}