wm_simulation/
rare_event.rs1use crate::bayesian::rand_u01;
18
19fn randn(state: &mut u64) -> f64 {
21 let u1 = rand_u01(state).max(1e-12);
22 let u2 = rand_u01(state).max(1e-12);
23 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
24}
25
26fn phi(x: f64) -> f64 {
28 (-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
29}
30
31#[allow(clippy::too_many_arguments)]
38pub fn subset_simulation<G>(
39 dim: usize,
40 n_per_level: usize,
41 n_levels: usize,
42 threshold: f64,
43 g: G,
44 seed: u64,
45 proposal_std: f64,
46) -> SubsetResult
47where
48 G: Fn(&[f64]) -> f64,
49{
50 let mut rng = seed;
51 let p0 = 0.1_f64; let mut samples: Vec<Vec<f64>> = (0..n_per_level)
54 .map(|_| (0..dim).map(|_| randn(&mut rng)).collect())
55 .collect();
56 let mut g_values: Vec<f64> = samples.iter().map(|s| g(s)).collect();
57
58 let mut level = 0usize;
59
60 while level < n_levels {
61 let mut sorted = g_values.clone();
63 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
64 let idx = ((n_per_level as f64) * (1.0 - p0)) as usize;
65 let level_threshold = sorted[idx.min(n_per_level - 1)];
66
67 if level_threshold >= threshold {
68 break;
71 }
72
73 let mut seeds: Vec<(Vec<f64>, f64)> = Vec::with_capacity(n_per_level);
75 for (s, gv) in samples.iter().zip(g_values.iter()) {
76 if *gv >= level_threshold {
77 seeds.push((s.clone(), *gv));
78 }
79 }
80 if seeds.is_empty() {
82 let mut pairs: Vec<(Vec<f64>, f64)> = samples.into_iter().zip(g_values).collect();
83 pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
84 seeds = pairs.into_iter().take(n_per_level.max(1)).collect();
85 }
86
87 let mut next: Vec<Vec<f64>> = Vec::with_capacity(n_per_level);
92 let mut next_g: Vec<f64> = Vec::with_capacity(n_per_level);
93 let n_seeds = seeds.len();
94 for i in 0..n_per_level {
95 let seed_sample = seeds[i % n_seeds].0.clone();
96 let mut candidate = seed_sample.clone();
97 let mut candidate_density = density(candidate.iter().copied());
98 let mut accepted = false;
99 for _ in 0..500 {
100 let mut trial = candidate.clone();
101 for v in &mut trial {
102 *v = proposal_std.mul_add(randn(&mut rng), *v);
103 }
104 let g_trial = g(&trial);
105 if g_trial < level_threshold {
106 continue;
107 }
108 let trial_density = density(trial.iter().copied());
109 let ratio = (trial_density / candidate_density).min(1.0);
110 if rand_u01(&mut rng) < ratio {
111 candidate = trial;
112 candidate_density = trial_density;
113 accepted = true;
114 break;
115 }
116 }
117 let gv = if accepted {
118 g(&candidate)
119 } else {
120 seeds[i % n_seeds].1
121 };
122 next_g.push(gv);
123 next.push(candidate);
124 }
125 samples = next;
126 g_values = next_g;
127 level += 1;
128 }
129
130 let fail = g_values.iter().filter(|&&v| v >= threshold).count();
132 let conditional = fail as f64 / g_values.len().max(1) as f64;
133 let probability = p0.powf(level as f64) * conditional;
134
135 SubsetResult {
136 probability,
137 levels_used: level,
138 n_samples_total: n_per_level * (level + 1),
139 method: "subset".into(),
140 }
141}
142
143pub fn importance_sampling<G>(
149 dim: usize,
150 n_samples: usize,
151 threshold: f64,
152 g: G,
153 seed: u64,
154) -> ImportanceResult
155where
156 G: Fn(&[f64]) -> f64,
157{
158 let mut rng = seed;
159 let shift = 0.5 * threshold.sqrt().min(8.0) / dim.max(1) as f64;
164
165 let mut count = 0usize;
166 let mut weight_sum = 0.0_f64;
167 for _ in 0..n_samples {
168 let x: Vec<f64> = (0..dim).map(|_| randn(&mut rng) + shift).collect();
169 let w = likelihood_ratio(&x, shift);
170 if g(&x) >= threshold {
171 count += 1;
172 weight_sum += w;
173 }
174 }
175 let mean = weight_sum / n_samples.max(1) as f64;
177 let cv = if mean > 1e-300 {
178 (count as f64).sqrt() / n_samples.max(1) as f64 / mean.max(1e-300)
180 } else {
181 0.0
182 };
183 ImportanceResult {
184 probability: mean,
185 coefficient_of_variation: cv,
186 hits: count,
187 n_samples,
188 method: "importance".into(),
189 }
190}
191
192fn likelihood_ratio(x: &[f64], shift: f64) -> f64 {
194 let mut lr = 1.0_f64;
195 for &xi in x {
196 lr *= (-xi).mul_add(shift, 0.5 * shift * shift).exp();
198 }
199 lr
200}
201
202fn density(x: impl Iterator<Item = f64>) -> f64 {
204 x.map(phi).product()
205}
206
207#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
209pub struct SubsetResult {
210 pub probability: f64,
212 pub levels_used: usize,
214 pub n_samples_total: usize,
216 pub method: String,
218}
219
220#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
222pub struct ImportanceResult {
223 pub probability: f64,
225 pub coefficient_of_variation: f64,
227 pub hits: usize,
229 pub n_samples: usize,
231 pub method: String,
233}
234
235#[cfg(test)]
236mod tests {
237 #![allow(clippy::suboptimal_flops)] use super::*;
239
240 #[test]
243 fn subset_matches_chi_square_tail() {
244 let result = subset_simulation(
245 2,
246 2000,
247 3,
248 9.0,
249 |x: &[f64]| x[0] * x[0] + x[1] * x[1],
250 42,
251 1.0,
252 );
253 assert!(
254 (result.probability - 0.0111).abs() < 0.01,
255 "subset: {} (expected ~0.0111)",
256 result.probability
257 );
258 assert!(result.levels_used >= 1);
259 }
260
261 #[test]
262 fn importance_matches_chi_square_tail() {
263 let result = importance_sampling(2, 50_000, 9.0, |x: &[f64]| x[0] * x[0] + x[1] * x[1], 7);
264 assert!(
265 (result.probability - 0.0111).abs() < 0.01,
266 "importance: {} (expected ~0.0111)",
267 result.probability
268 );
269 }
270
271 #[test]
272 fn subset_common_event_is_close() {
273 let result = subset_simulation(
275 2,
276 2000,
277 2,
278 4.0,
279 |x: &[f64]| x[0] * x[0] + x[1] * x[1],
280 123,
281 1.0,
282 );
283 assert!(
284 (result.probability - 0.1353).abs() < 0.05,
285 "subset: {} (expected ~0.135)",
286 result.probability
287 );
288 }
289
290 #[test]
291 fn importance_rejects_no_hits_gracefully() {
292 let result = importance_sampling(2, 1000, 1e9, |x: &[f64]| x[0] * x[0] + x[1] * x[1], 1);
294 assert!(result.probability < 1e-3);
295 assert_eq!(result.hits, 0);
296 }
297
298 #[test]
299 fn subset_always_exceeded_is_one() {
300 let result = subset_simulation(
302 2,
303 500,
304 1,
305 -1.0,
306 |x: &[f64]| x[0] * x[0] + x[1] * x[1],
307 9,
308 1.0,
309 );
310 assert!(result.probability > 0.99);
311 }
312}