Skip to main content

sciforge_hub/tools/
deterministic.rs

1//! Deterministic computation tools: reproducible RNG (xoshiro256**),
2//! numeric fingerprinting (FNV-1a), Kahan summation, and auditable context.
3
4/// Pseudo-random number generator (xoshiro256**) with fixed seed.
5#[derive(Debug, Clone)]
6pub struct Rng {
7    s: [u64; 4],
8}
9
10impl Rng {
11    /// Creates an RNG from `seed` via SplitMix64 mixing.
12    pub fn new(seed: u64) -> Self {
13        let mut z = seed;
14        let mut s = [0u64; 4];
15        for si in &mut s {
16            z = z.wrapping_add(0x9e3779b97f4a7c15);
17            z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
18            z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
19            *si = z ^ (z >> 31);
20        }
21        Self { s }
22    }
23
24    /// Generates the next 64-bit integer.
25    pub fn next_u64(&mut self) -> u64 {
26        let result = (self.s[1].wrapping_mul(5)).rotate_left(7).wrapping_mul(9);
27        let t = self.s[1] << 17;
28        self.s[2] ^= self.s[0];
29        self.s[3] ^= self.s[1];
30        self.s[1] ^= self.s[2];
31        self.s[0] ^= self.s[3];
32        self.s[2] ^= t;
33        self.s[3] = self.s[3].rotate_left(45);
34        result
35    }
36
37    /// Generates a uniform float in `[0, 1)`.
38    pub fn next_f64(&mut self) -> f64 {
39        (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
40    }
41
42    /// Generates a uniform float in `[lo, hi)`.
43    pub fn uniform(&mut self, lo: f64, hi: f64) -> f64 {
44        lo + self.next_f64() * (hi - lo)
45    }
46
47    /// Generates a uniform index in `[0, n)`.
48    pub fn next_usize(&mut self, n: usize) -> usize {
49        (self.next_u64() % n as u64) as usize
50    }
51
52    /// Generates a standard normal sample (Box-Muller).
53    pub fn normal(&mut self) -> f64 {
54        let u1 = self.next_f64().max(1e-300);
55        let u2 = self.next_f64();
56        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
57    }
58
59    /// Generates a normal sample with parameters `(mu, sigma)`.
60    pub fn normal_params(&mut self, mu: f64, sigma: f64) -> f64 {
61        mu + sigma * self.normal()
62    }
63
64    /// Fills `buf` with uniform values in `[0, 1)`.
65    pub fn fill_uniform(&mut self, buf: &mut [f64]) {
66        for v in buf.iter_mut() {
67            *v = self.next_f64();
68        }
69    }
70
71    /// Fills `buf` with standard normal values.
72    pub fn fill_normal(&mut self, buf: &mut [f64]) {
73        for v in buf.iter_mut() {
74            *v = self.normal();
75        }
76    }
77
78    /// Shuffles `data` in place (Fisher-Yates).
79    pub fn shuffle<T>(&mut self, data: &mut [T]) {
80        let n = data.len();
81        for i in (1..n).rev() {
82            let j = self.next_usize(i + 1);
83            data.swap(i, j);
84        }
85    }
86
87    /// Generates `n` uniform samples in `[lo, hi)`.
88    pub fn sample_uniform(&mut self, n: usize, lo: f64, hi: f64) -> Vec<f64> {
89        (0..n).map(|_| self.uniform(lo, hi)).collect()
90    }
91
92    /// Generates `n` normal samples with parameters `(mu, sigma)`.
93    pub fn sample_normal(&mut self, n: usize, mu: f64, sigma: f64) -> Vec<f64> {
94        (0..n).map(|_| self.normal_params(mu, sigma)).collect()
95    }
96}
97
98/// Computes the FNV-1a fingerprint of a `f64` slice.
99pub fn fingerprint(data: &[f64]) -> u64 {
100    let mut h: u64 = 0xcbf29ce484222325;
101    for &v in data {
102        let bytes = v.to_le_bytes();
103        for &b in &bytes {
104            h ^= b as u64;
105            h = h.wrapping_mul(0x100000001b3);
106        }
107    }
108    h
109}
110
111/// Computes the fingerprint of a single scalar.
112pub fn fingerprint_scalar(v: f64) -> u64 {
113    fingerprint(&[v])
114}
115
116/// Checks whether two fingerprints are equal.
117pub fn fingerprints_match(a: u64, b: u64) -> bool {
118    a == b
119}
120
121/// Reproducible context: fixed-seed RNG with audit trail.
122#[derive(Debug, Clone)]
123pub struct ReproducibleContext {
124    /// Initial seed used to initialize the RNG.
125    pub seed: u64,
126    /// Pseudo-random number generator.
127    pub rng: Rng,
128    /// Log of operations performed.
129    pub audit_trail: Vec<String>,
130}
131
132impl ReproducibleContext {
133    /// Creates a new reproducible context with the given seed.
134    pub fn new(seed: u64) -> Self {
135        Self {
136            seed,
137            rng: Rng::new(seed),
138            audit_trail: Vec::new(),
139        }
140    }
141
142    /// Appends an entry to the audit trail.
143    pub fn log(&mut self, entry: &str) {
144        self.audit_trail.push(entry.to_string());
145    }
146
147    /// Resets the RNG and clears the audit trail.
148    pub fn reset(&mut self) {
149        self.rng = Rng::new(self.seed);
150        self.audit_trail.clear();
151    }
152
153    /// Creates a child context derived from the current seed and `sub_seed`.
154    pub fn fork(&self, sub_seed: u64) -> Self {
155        let combined = self
156            .seed
157            .wrapping_mul(0x9e3779b97f4a7c15)
158            .wrapping_add(sub_seed);
159        Self::new(combined)
160    }
161
162    /// Returns a human-readable summary of the audit trail.
163    pub fn audit_summary(&self) -> String {
164        let mut out = format!("Seed: {}\nSteps: {}\n", self.seed, self.audit_trail.len());
165        for (i, entry) in self.audit_trail.iter().enumerate() {
166            out.push_str(&format!("  [{}] {}\n", i + 1, entry));
167        }
168        out
169    }
170}
171
172/// Checks that `computed` is close to `expected` within relative tolerance `tol`.
173pub fn assert_reproducible(computed: f64, expected: f64, tol: f64) -> bool {
174    if expected == 0.0 {
175        computed.abs() <= tol
176    } else {
177        ((computed - expected) / expected).abs() <= tol
178    }
179}
180
181/// Kahan compensated summation to reduce floating-point rounding errors.
182pub fn kahan_sum(data: &[f64]) -> f64 {
183    let mut sum = 0.0;
184    let mut c = 0.0;
185    for &x in data {
186        let y = x - c;
187        let t = sum + y;
188        c = (t - sum) - y;
189        sum = t;
190    }
191    sum
192}
193
194/// Kahan compensated dot product.
195pub fn kahan_dot(a: &[f64], b: &[f64]) -> f64 {
196    let mut sum = 0.0;
197    let mut c = 0.0;
198    let n = a.len().min(b.len());
199    for i in 0..n {
200        let y = a[i] * b[i] - c;
201        let t = sum + y;
202        c = (t - sum) - y;
203        sum = t;
204    }
205    sum
206}