qualia_core_db/solvers/learning/sampling/
mcmc.rs1#[derive(Debug, Clone)]
11pub struct McmcResult {
12 pub samples: Vec<f64>,
14 pub n_samples: usize,
15 pub dim: usize,
16 pub acceptance_rate: f64,
18}
19
20impl McmcResult {
21 pub fn mean(&self) -> Vec<f64> {
23 let mut m = vec![0.0; self.dim];
24 for i in 0..self.n_samples {
25 for j in 0..self.dim {
26 m[j] += self.samples[i * self.dim + j];
27 }
28 }
29 for v in m.iter_mut() {
30 *v /= self.n_samples.max(1) as f64;
31 }
32 m
33 }
34
35 pub fn variance(&self) -> Vec<f64> {
37 let m = self.mean();
38 let mut v = vec![0.0; self.dim];
39 for i in 0..self.n_samples {
40 for j in 0..self.dim {
41 let d = self.samples[i * self.dim + j] - m[j];
42 v[j] += d * d;
43 }
44 }
45 for x in v.iter_mut() {
46 *x /= self.n_samples.max(1) as f64;
47 }
48 v
49 }
50}
51
52struct Rng(u64);
53impl Rng {
54 fn unit(&mut self) -> f64 {
55 self.0 = self
56 .0
57 .wrapping_mul(6364136223846793005)
58 .wrapping_add(1442695040888963407);
59 ((self.0 >> 11) as f64) / ((1u64 << 53) as f64)
60 }
61 fn gaussian(&mut self) -> f64 {
62 let u1 = self.unit().max(1e-12);
63 let u2 = self.unit();
64 (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
65 }
66}
67
68pub fn metropolis_hastings<F>(
73 log_density: F,
74 initial: &[f64],
75 proposal_std: f64,
76 n_samples: usize,
77 burn_in: usize,
78 seed: u64,
79) -> McmcResult
80where
81 F: Fn(&[f64]) -> f64,
82{
83 let dim = initial.len();
84 let mut rng = Rng(seed ^ 0x9E3779B97F4A7C15);
85 let mut x = initial.to_vec();
86 let mut lp = log_density(&x);
87 let mut prop = vec![0.0; dim];
88 let mut samples = Vec::with_capacity(n_samples * dim);
89 let mut accepted = 0u64;
90 let total = burn_in + n_samples;
91
92 for step in 0..total {
93 for d in 0..dim {
95 prop[d] = x[d] + proposal_std * rng.gaussian();
96 }
97 let lp_prop = log_density(&prop);
98 let accept = lp_prop >= lp || rng.unit() < (lp_prop - lp).exp();
100 if accept {
101 x.copy_from_slice(&prop);
102 lp = lp_prop;
103 if step >= burn_in {
104 accepted += 1;
105 }
106 }
107 if step >= burn_in {
108 samples.extend_from_slice(&x);
109 }
110 }
111
112 McmcResult {
113 samples,
114 n_samples,
115 dim,
116 acceptance_rate: if n_samples > 0 {
117 accepted as f64 / n_samples as f64
118 } else {
119 0.0
120 },
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn samples_a_1d_gaussian() {
130 let target = |x: &[f64]| -(x[0] - 3.0) * (x[0] - 3.0) / 8.0;
132 let r = metropolis_hastings(target, &[0.0], 2.0, 40_000, 5_000, 1);
133 let m = r.mean();
134 let v = r.variance();
135 assert!((m[0] - 3.0).abs() < 0.15, "mean {}", m[0]);
136 assert!((v[0] - 4.0).abs() < 0.6, "var {}", v[0]);
137 assert!(r.acceptance_rate > 0.1 && r.acceptance_rate < 0.95);
138 }
139
140 #[test]
141 fn samples_a_2d_gaussian_mean() {
142 let target = |x: &[f64]| -(x[0] - 1.0).powi(2) / 2.0 - (x[1] + 2.0).powi(2) / 2.0;
144 let r = metropolis_hastings(target, &[0.0, 0.0], 1.0, 40_000, 5_000, 7);
145 let m = r.mean();
146 assert!(
147 (m[0] - 1.0).abs() < 0.15 && (m[1] + 2.0).abs() < 0.15,
148 "mean {m:?}"
149 );
150 }
151
152 #[test]
153 fn unnormalized_target_is_fine() {
154 let target = |x: &[f64]| 12345.0 - (x[0]).powi(2) / 2.0; let r = metropolis_hastings(target, &[0.0], 1.5, 20_000, 3_000, 2);
157 assert!(r.mean()[0].abs() < 0.15);
158 assert!((r.variance()[0] - 1.0).abs() < 0.3);
159 }
160}