Skip to main content

oxiphysics_core/
monte_carlo.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Monte Carlo methods for physics simulation.
6//!
7//! Provides sampling distributions, Markov Chain Monte Carlo (MCMC),
8//! Hamiltonian Monte Carlo, Monte Carlo integration (stratified, LHS,
9//! quasi-Monte Carlo), bootstrap confidence intervals, particle filters,
10//! and Brownian bridge processes.
11
12#![allow(dead_code)]
13
14use std::f64::consts::PI;
15
16// ─────────────────────────────────────────────────────────────────────────────
17// Local LCG RNG (same style as stochastic.rs)
18// ─────────────────────────────────────────────────────────────────────────────
19
20/// A fast linear congruential random number generator.
21#[allow(dead_code)]
22pub struct LcgRng {
23    state: u64,
24}
25
26impl LcgRng {
27    fn new(seed: u64) -> Self {
28        Self { state: seed.max(1) }
29    }
30
31    fn next_u64(&mut self) -> u64 {
32        self.state = self
33            .state
34            .wrapping_mul(6_364_136_223_846_793_005)
35            .wrapping_add(1_442_695_040_888_963_407);
36        self.state
37    }
38
39    fn next_f64(&mut self) -> f64 {
40        (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
41    }
42
43    fn next_normal(&mut self) -> f64 {
44        box_muller_normal(self.next_f64(), self.next_f64())
45    }
46}
47
48// ─────────────────────────────────────────────────────────────────────────────
49// Helper functions
50// ─────────────────────────────────────────────────────────────────────────────
51
52/// Box-Muller transform: generates a standard normal N(0,1) from two U(0,1) samples.
53///
54/// # Arguments
55/// * `u1` - Uniform sample in (0, 1].
56/// * `u2` - Uniform sample in \[0, 1).
57pub fn box_muller_normal(u1: f64, u2: f64) -> f64 {
58    let u1 = u1.max(1e-15);
59    (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
60}
61
62/// Inverse CDF of the standard normal distribution (Beasley-Springer-Moro approximation).
63///
64/// Maps p ∈ (0, 1) to the quantile x such that P(X ≤ x) = p for X ~ N(0,1).
65pub fn inverse_cdf(p: f64) -> f64 {
66    // Rational approximation (Abramowitz and Stegun 26.2.17)
67    assert!(p > 0.0 && p < 1.0, "p must be in (0, 1)");
68    let p_adj = if p < 0.5 { p } else { 1.0 - p };
69    let t = (-2.0 * p_adj.ln()).sqrt();
70    let c0 = 2.515_517;
71    let c1 = 0.802_853;
72    let c2 = 0.010_328;
73    let d1 = 1.432_788;
74    let d2 = 0.189_269;
75    let d3 = 0.001_308;
76    let num = c0 + c1 * t + c2 * t * t;
77    let den = 1.0 + d1 * t + d2 * t * t + d3 * t * t * t;
78    let x = t - num / den;
79    if p >= 0.5 { x } else { -x }
80}
81
82/// Computes the effective sample size (ESS) from normalized importance weights.
83///
84/// ESS = (Σ w_i)^2 / Σ w_i^2.
85pub fn effective_sample_size(weights: &[f64]) -> f64 {
86    let sum_w: f64 = weights.iter().sum();
87    if sum_w < 1e-15 {
88        return 0.0;
89    }
90    let sum_w2: f64 = weights.iter().map(|&w| (w / sum_w).powi(2)).sum();
91    1.0 / sum_w2
92}
93
94/// Computes the autocorrelation of a time series at lag `k`.
95///
96/// Returns the normalized autocorrelation coefficient r(k) ∈ \[-1, 1\].
97pub fn autocorrelation(x: &[f64], k: usize) -> f64 {
98    let n = x.len();
99    if k >= n {
100        return 0.0;
101    }
102    let mean = x.iter().sum::<f64>() / n as f64;
103    let var: f64 = x.iter().map(|&xi| (xi - mean).powi(2)).sum::<f64>() / n as f64;
104    if var < 1e-15 {
105        return 1.0;
106    }
107    let cov: f64 = (0..n - k)
108        .map(|i| (x[i] - mean) * (x[i + k] - mean))
109        .sum::<f64>()
110        / n as f64;
111    cov / var
112}
113
114// ─────────────────────────────────────────────────────────────────────────────
115// McSampler — probability distribution sampler
116// ─────────────────────────────────────────────────────────────────────────────
117
118/// Monte Carlo sampler supporting multiple probability distributions.
119///
120/// Uses an internal LCG RNG; set the seed for reproducibility.
121pub struct McSampler {
122    rng: LcgRng,
123}
124
125impl McSampler {
126    /// Creates a new sampler with the given seed.
127    pub fn new(seed: u64) -> Self {
128        Self {
129            rng: LcgRng::new(seed),
130        }
131    }
132
133    /// Samples from Uniform(0, 1).
134    pub fn uniform(&mut self) -> f64 {
135        self.rng.next_f64()
136    }
137
138    /// Samples from Uniform(a, b).
139    pub fn uniform_range(&mut self, a: f64, b: f64) -> f64 {
140        a + (b - a) * self.rng.next_f64()
141    }
142
143    /// Samples from Normal(mu, sigma).
144    pub fn normal(&mut self, mu: f64, sigma: f64) -> f64 {
145        mu + sigma * self.rng.next_normal()
146    }
147
148    /// Samples from Exponential(lambda): pdf = lambda * exp(-lambda * x).
149    pub fn exponential(&mut self, lambda: f64) -> f64 {
150        -self.rng.next_f64().max(1e-15).ln() / lambda
151    }
152
153    /// Samples from Poisson(lambda) using Knuth's algorithm.
154    pub fn poisson(&mut self, lambda: f64) -> u64 {
155        let limit = (-lambda).exp();
156        let mut k = 0u64;
157        let mut p = 1.0;
158        loop {
159            p *= self.rng.next_f64();
160            if p <= limit {
161                break;
162            }
163            k += 1;
164        }
165        k
166    }
167
168    /// Samples from Beta(alpha, beta) using Johnk's method.
169    pub fn beta(&mut self, alpha: f64, beta: f64) -> f64 {
170        // Use gamma ratio: Beta(a,b) = Gamma(a) / (Gamma(a) + Gamma(b))
171        let x = self.gamma(alpha, 1.0);
172        let y = self.gamma(beta, 1.0);
173        x / (x + y)
174    }
175
176    /// Samples from Gamma(shape, scale) using Marsaglia-Tsang method.
177    pub fn gamma(&mut self, shape: f64, scale: f64) -> f64 {
178        if shape < 1.0 {
179            // Use Gamma(shape+1) * U^(1/shape)
180            let g = self.gamma(shape + 1.0, scale);
181            let u = self.rng.next_f64().max(1e-15);
182            return g * u.powf(1.0 / shape);
183        }
184        let d = shape - 1.0 / 3.0;
185        let c = 1.0 / (9.0 * d).sqrt();
186        loop {
187            let z = self.rng.next_normal();
188            let v_inner = 1.0 + c * z;
189            if v_inner <= 0.0 {
190                continue;
191            }
192            let v = v_inner.powi(3);
193            let u = self.rng.next_f64().max(1e-15);
194            if u < 1.0 - 0.0331 * z.powi(4) {
195                return d * v * scale;
196            }
197            if u.ln() < 0.5 * z * z + d * (1.0 - v + v.ln()) {
198                return d * v * scale;
199            }
200        }
201    }
202
203    /// Draws `n` samples from Normal(mu, sigma).
204    pub fn normal_samples(&mut self, n: usize, mu: f64, sigma: f64) -> Vec<f64> {
205        (0..n).map(|_| self.normal(mu, sigma)).collect()
206    }
207
208    /// Draws `n` samples from Uniform(a, b).
209    pub fn uniform_samples(&mut self, n: usize, a: f64, b: f64) -> Vec<f64> {
210        (0..n).map(|_| self.uniform_range(a, b)).collect()
211    }
212}
213
214// ─────────────────────────────────────────────────────────────────────────────
215// ImportanceSampler
216// ─────────────────────────────────────────────────────────────────────────────
217
218/// Importance sampler with acceptance-rejection and adaptive IS weights.
219///
220/// Uses a proposal distribution q(x) to estimate expectations under target p(x).
221pub struct ImportanceSampler {
222    rng: LcgRng,
223    /// Proposal distribution standard deviation.
224    pub proposal_sigma: f64,
225}
226
227impl ImportanceSampler {
228    /// Creates a new importance sampler.
229    pub fn new(seed: u64, proposal_sigma: f64) -> Self {
230        Self {
231            rng: LcgRng::new(seed),
232            proposal_sigma,
233        }
234    }
235
236    /// Acceptance-rejection sampler for target pdf `target` with envelope `c * proposal`.
237    ///
238    /// Returns a sample from the target distribution.
239    pub fn accept_reject<F, G>(&mut self, target: F, proposal_sample: G, c: f64) -> f64
240    where
241        F: Fn(f64) -> f64,
242        G: Fn(&mut LcgRng) -> (f64, f64), // returns (sample, proposal_density)
243    {
244        loop {
245            let (x, q_x) = proposal_sample(&mut self.rng);
246            let p_x = target(x);
247            let u = self.rng.next_f64();
248            if u <= p_x / (c * q_x) {
249                return x;
250            }
251        }
252    }
253
254    /// Computes unnormalized importance weights for samples drawn from proposal.
255    ///
256    /// w_i = target(x_i) / proposal(x_i).
257    pub fn importance_weights<F, G>(&self, samples: &[f64], target: F, proposal: G) -> Vec<f64>
258    where
259        F: Fn(f64) -> f64,
260        G: Fn(f64) -> f64,
261    {
262        samples
263            .iter()
264            .map(|&x| {
265                let q = proposal(x).max(1e-300);
266                target(x) / q
267            })
268            .collect()
269    }
270
271    /// Estimates E_p\[f(x)\] using importance sampling from proposal q.
272    pub fn estimate<F, H>(&mut self, n: usize, f_fn: F, log_w_fn: H) -> f64
273    where
274        F: Fn(f64) -> f64,
275        H: Fn(f64) -> f64, // log(target/proposal) at x
276    {
277        let sigma = self.proposal_sigma;
278        let samples: Vec<f64> = (0..n).map(|_| self.rng.next_normal() * sigma).collect();
279        let log_w: Vec<f64> = samples.iter().map(|&x| log_w_fn(x)).collect();
280        let log_w_max = log_w.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
281        let w: Vec<f64> = log_w.iter().map(|&lw| (lw - log_w_max).exp()).collect();
282        let sum_w: f64 = w.iter().sum();
283        let numerator: f64 = samples
284            .iter()
285            .zip(w.iter())
286            .map(|(&x, &wi)| f_fn(x) * wi)
287            .sum();
288        numerator / sum_w
289    }
290}
291
292// ─────────────────────────────────────────────────────────────────────────────
293// Estimator — statistical estimator from samples
294// ─────────────────────────────────────────────────────────────────────────────
295
296/// Statistical estimator: computes mean, variance, confidence intervals, and ESS.
297pub struct Estimator {
298    /// The sample data.
299    pub samples: Vec<f64>,
300}
301
302impl Estimator {
303    /// Creates a new estimator from a sample vector.
304    pub fn new(samples: Vec<f64>) -> Self {
305        Self { samples }
306    }
307
308    /// Sample mean.
309    pub fn mean(&self) -> f64 {
310        let n = self.samples.len();
311        if n == 0 {
312            return 0.0;
313        }
314        self.samples.iter().sum::<f64>() / n as f64
315    }
316
317    /// Sample variance (unbiased, divides by n-1).
318    pub fn variance(&self) -> f64 {
319        let n = self.samples.len();
320        if n < 2 {
321            return 0.0;
322        }
323        let mu = self.mean();
324        self.samples.iter().map(|&x| (x - mu).powi(2)).sum::<f64>() / (n - 1) as f64
325    }
326
327    /// Standard error of the mean: SE = sqrt(variance / n).
328    pub fn standard_error(&self) -> f64 {
329        let n = self.samples.len();
330        if n == 0 {
331            return 0.0;
332        }
333        (self.variance() / n as f64).sqrt()
334    }
335
336    /// Confidence interval at level `alpha` (e.g., alpha=0.05 for 95% CI).
337    ///
338    /// Returns (lower, upper) assuming normality of the estimator.
339    pub fn confidence_interval(&self, alpha: f64) -> (f64, f64) {
340        let mu = self.mean();
341        let se = self.standard_error();
342        // z-score for alpha/2
343        let z = inverse_cdf(1.0 - alpha / 2.0);
344        (mu - z * se, mu + z * se)
345    }
346
347    /// Effective sample size using the autocorrelation-based estimate.
348    ///
349    /// ESS = n / (1 + 2 * Σ_k ρ(k)) where ρ(k) is the autocorrelation at lag k.
350    pub fn effective_sample_size(&self) -> f64 {
351        let n = self.samples.len();
352        if n == 0 {
353            return 0.0;
354        }
355        let mut sum_rho = 0.0;
356        let max_lag = (n / 2).min(50);
357        for k in 1..max_lag {
358            let rho = autocorrelation(&self.samples, k);
359            if rho.abs() < 0.05 {
360                break;
361            }
362            sum_rho += rho;
363        }
364        n as f64 / (1.0 + 2.0 * sum_rho).max(0.01)
365    }
366
367    /// Median of the samples.
368    pub fn median(&self) -> f64 {
369        let n = self.samples.len();
370        if n == 0 {
371            return 0.0;
372        }
373        let mut sorted = self.samples.clone();
374        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
375        if n % 2 == 1 {
376            sorted[n / 2]
377        } else {
378            0.5 * (sorted[n / 2 - 1] + sorted[n / 2])
379        }
380    }
381}
382
383// ─────────────────────────────────────────────────────────────────────────────
384// MetropolisHastings
385// ─────────────────────────────────────────────────────────────────────────────
386
387/// Metropolis-Hastings MCMC sampler.
388///
389/// Generates a Markov chain targeting the distribution proportional to `log_target(x)`.
390pub struct MetropolisHastings {
391    rng: LcgRng,
392    /// Proposal step size (standard deviation of Gaussian proposal).
393    pub step_size: f64,
394    /// Number of burn-in steps to discard.
395    pub burn_in: usize,
396    /// Thinning interval (keep every k-th sample).
397    pub thinning: usize,
398    /// Number of accepted proposals (updated during sampling).
399    pub n_accepted: usize,
400}
401
402impl MetropolisHastings {
403    /// Creates a new MH sampler.
404    pub fn new(seed: u64, step_size: f64, burn_in: usize, thinning: usize) -> Self {
405        Self {
406            rng: LcgRng::new(seed),
407            step_size,
408            burn_in,
409            thinning,
410            n_accepted: 0,
411        }
412    }
413
414    /// Runs the MH chain for `n_samples` (after burn-in), targeting `log_target`.
415    ///
416    /// `x0` is the initial state. Returns a vector of samples.
417    pub fn sample<F>(&mut self, x0: f64, n_samples: usize, log_target: F) -> Vec<f64>
418    where
419        F: Fn(f64) -> f64,
420    {
421        let mut x = x0;
422        let total = self.burn_in + n_samples * self.thinning;
423        let mut chain = Vec::with_capacity(n_samples);
424        self.n_accepted = 0;
425        let mut n_proposed = 0usize;
426
427        for step in 0..total {
428            let x_prop = x + self.step_size * self.rng.next_normal();
429            let log_alpha = (log_target(x_prop) - log_target(x)).min(0.0);
430            let u = self.rng.next_f64().max(1e-15).ln();
431            n_proposed += 1;
432            if u <= log_alpha {
433                x = x_prop;
434                if step >= self.burn_in {
435                    self.n_accepted += 1;
436                }
437            }
438            if step >= self.burn_in && (step - self.burn_in).is_multiple_of(self.thinning) {
439                chain.push(x);
440            }
441        }
442        let _ = n_proposed;
443        chain
444    }
445
446    /// Acceptance rate of the chain.
447    pub fn acceptance_rate(&self, n_total: usize) -> f64 {
448        if n_total == 0 {
449            0.0
450        } else {
451            self.n_accepted as f64 / n_total as f64
452        }
453    }
454
455    /// Chain diagnostics: returns (mean, variance, ESS).
456    pub fn diagnostics(&self, chain: &[f64]) -> (f64, f64, f64) {
457        let est = Estimator::new(chain.to_vec());
458        (est.mean(), est.variance(), est.effective_sample_size())
459    }
460}
461
462// ─────────────────────────────────────────────────────────────────────────────
463// HamiltonianMC — HMC with leapfrog integrator
464// ─────────────────────────────────────────────────────────────────────────────
465
466/// Hamiltonian Monte Carlo sampler.
467///
468/// Uses the leapfrog integrator to propose far-reaching moves along
469/// level sets of the Hamiltonian H(q, p) = U(q) + K(p).
470pub struct HamiltonianMC {
471    rng: LcgRng,
472    /// Leapfrog step size.
473    pub step_size: f64,
474    /// Number of leapfrog steps per proposal.
475    pub n_leapfrog: usize,
476    /// Number of burn-in steps.
477    pub burn_in: usize,
478    /// Acceptance count.
479    pub n_accepted: usize,
480}
481
482impl HamiltonianMC {
483    /// Creates a new HMC sampler.
484    pub fn new(seed: u64, step_size: f64, n_leapfrog: usize, burn_in: usize) -> Self {
485        Self {
486            rng: LcgRng::new(seed),
487            step_size,
488            n_leapfrog,
489            burn_in,
490            n_accepted: 0,
491        }
492    }
493
494    /// Kinetic energy K(p) = 0.5 * p^2 (unit mass Gaussian momentum).
495    pub fn kinetic_energy(p: f64) -> f64 {
496        0.5 * p * p
497    }
498
499    /// One leapfrog trajectory of `n_steps` steps.
500    ///
501    /// Returns (q_new, p_new) after the trajectory.
502    pub fn leapfrog<F>(&self, q0: f64, p0: f64, grad_u: F) -> (f64, f64)
503    where
504        F: Fn(f64) -> f64,
505    {
506        let eps = self.step_size;
507        let mut q = q0;
508        let mut p = p0;
509        // Half step for momentum
510        p -= 0.5 * eps * grad_u(q);
511        for _ in 0..self.n_leapfrog {
512            q += eps * p;
513            let is_last = false;
514            let _ = is_last;
515            p -= eps * grad_u(q);
516        }
517        // Undo last full step, do half step
518        p += eps * grad_u(q);
519        p -= 0.5 * eps * grad_u(q);
520        (q, p)
521    }
522
523    /// Runs HMC for `n_samples` after burn-in.
524    ///
525    /// `log_target` is the log of the unnormalized target density.
526    /// `grad_u` = -d(log_target)/dq (gradient of potential energy).
527    pub fn sample<F, G>(&mut self, q0: f64, n_samples: usize, log_target: F, grad_u: G) -> Vec<f64>
528    where
529        F: Fn(f64) -> f64,
530        G: Fn(f64) -> f64,
531    {
532        let total = self.burn_in + n_samples;
533        let mut q = q0;
534        let mut chain = Vec::with_capacity(n_samples);
535        self.n_accepted = 0;
536
537        for step in 0..total {
538            let p = self.rng.next_normal();
539            let h_current = -log_target(q) + Self::kinetic_energy(p);
540            let (q_prop, p_prop) = self.leapfrog(q, p, &grad_u);
541            let h_prop = -log_target(q_prop) + Self::kinetic_energy(p_prop);
542            let delta_h = h_prop - h_current;
543            let u = self.rng.next_f64().max(1e-15).ln();
544            if u <= -delta_h.max(0.0) || delta_h <= 0.0 {
545                q = q_prop;
546                if step >= self.burn_in {
547                    self.n_accepted += 1;
548                }
549            }
550            if step >= self.burn_in {
551                chain.push(q);
552            }
553        }
554        chain
555    }
556}
557
558// ─────────────────────────────────────────────────────────────────────────────
559// MarkovChainMC — general MCMC dispatcher
560// ─────────────────────────────────────────────────────────────────────────────
561
562/// General Markov Chain Monte Carlo runner.
563///
564/// Wraps Metropolis-Hastings and provides Gibbs sampling interface.
565pub struct MarkovChainMC {
566    rng: LcgRng,
567    /// Step size for proposal distributions.
568    pub step_size: f64,
569}
570
571impl MarkovChainMC {
572    /// Creates a new MCMC runner.
573    pub fn new(seed: u64, step_size: f64) -> Self {
574        Self {
575            rng: LcgRng::new(seed),
576            step_size,
577        }
578    }
579
580    /// Metropolis-Hastings for a univariate log-target.
581    pub fn metropolis_hastings<F>(
582        &mut self,
583        x0: f64,
584        n_samples: usize,
585        burn_in: usize,
586        log_target: F,
587    ) -> Vec<f64>
588    where
589        F: Fn(f64) -> f64,
590    {
591        let mut mh = MetropolisHastings::new(42, self.step_size, burn_in, 1);
592        mh.sample(x0, n_samples, log_target)
593    }
594
595    /// Gibbs sampler for a bivariate target with full conditionals.
596    ///
597    /// `sample_x_given_y` and `sample_y_given_x` are samplers for each conditional.
598    pub fn gibbs_2d<F, G>(
599        &mut self,
600        x0: f64,
601        y0: f64,
602        n_samples: usize,
603        burn_in: usize,
604        sample_x: F,
605        sample_y: G,
606    ) -> Vec<(f64, f64)>
607    where
608        F: Fn(f64, &mut LcgRng) -> f64, // sample x | y
609        G: Fn(f64, &mut LcgRng) -> f64, // sample y | x
610    {
611        let total = burn_in + n_samples;
612        let mut chain = Vec::with_capacity(n_samples);
613        // Start chain at the provided initial point
614        let mut state = (x0, y0);
615        for step in 0..total {
616            let new_x = sample_x(state.1, &mut self.rng);
617            let new_y = sample_y(new_x, &mut self.rng);
618            state = (new_x, new_y);
619            if step >= burn_in {
620                chain.push(state);
621            }
622        }
623        chain
624    }
625
626    /// MALA (Metropolis-Adjusted Langevin Algorithm) sampler.
627    ///
628    /// Proposal: x_prop = x + (eps^2/2) * grad_log_target(x) + eps * N(0,1).
629    pub fn mala<F, G>(
630        &mut self,
631        x0: f64,
632        n_samples: usize,
633        eps: f64,
634        log_target: F,
635        grad_log_target: G,
636    ) -> Vec<f64>
637    where
638        F: Fn(f64) -> f64,
639        G: Fn(f64) -> f64,
640    {
641        let mut x = x0;
642        let mut chain = Vec::with_capacity(n_samples);
643        for _ in 0..n_samples {
644            let g = grad_log_target(x);
645            let x_prop = x + 0.5 * eps * eps * g + eps * self.rng.next_normal();
646            // MH correction
647            let log_alpha = log_target(x_prop) - log_target(x);
648            if self.rng.next_f64().max(1e-15).ln() <= log_alpha.min(0.0) {
649                x = x_prop;
650            }
651            chain.push(x);
652        }
653        chain
654    }
655}
656
657// ─────────────────────────────────────────────────────────────────────────────
658// MonteCarloIntegral — multi-dimensional quadrature
659// ─────────────────────────────────────────────────────────────────────────────
660
661/// Monte Carlo integration methods: plain, stratified, LHS, and quasi-MC.
662pub struct MonteCarloIntegral {
663    rng: LcgRng,
664    /// Number of integration samples.
665    pub n_samples: usize,
666    /// Dimensionality of the integration domain.
667    pub dim: usize,
668}
669
670impl MonteCarloIntegral {
671    /// Creates a new Monte Carlo integrator.
672    pub fn new(seed: u64, n_samples: usize, dim: usize) -> Self {
673        Self {
674            rng: LcgRng::new(seed),
675            n_samples,
676            dim,
677        }
678    }
679
680    /// Plain Monte Carlo integration of `f` over the unit hypercube \[0,1\]^d.
681    ///
682    /// Returns (estimate, standard_error).
683    pub fn integrate_plain<F>(&mut self, f: F) -> (f64, f64)
684    where
685        F: Fn(&[f64]) -> f64,
686    {
687        let n = self.n_samples;
688        let d = self.dim;
689        let mut sum = 0.0;
690        let mut sum_sq = 0.0;
691        let mut x = vec![0.0_f64; d];
692        for _ in 0..n {
693            for xi in x.iter_mut() {
694                *xi = self.rng.next_f64();
695            }
696            let val = f(&x);
697            sum += val;
698            sum_sq += val * val;
699        }
700        let mean = sum / n as f64;
701        let var = (sum_sq / n as f64 - mean * mean).max(0.0);
702        (mean, (var / n as f64).sqrt())
703    }
704
705    /// Stratified Monte Carlo: divides each dimension into k strata.
706    ///
707    /// Returns (estimate, standard_error).
708    pub fn integrate_stratified<F>(&mut self, f: F, k: usize) -> (f64, f64)
709    where
710        F: Fn(&[f64]) -> f64,
711    {
712        let d = self.dim;
713        let n_strata = k.pow(d as u32);
714        let mut sum = 0.0;
715        let mut sum_sq = 0.0;
716        let vol = 1.0 / n_strata as f64;
717        for s in 0..n_strata {
718            let mut x = vec![0.0_f64; d];
719            let mut idx = s;
720            for j in 0..d {
721                let cell = idx % k;
722                idx /= k;
723                x[j] = (cell as f64 + self.rng.next_f64()) / k as f64;
724            }
725            let val = f(&x) * vol * n_strata as f64;
726            sum += val;
727            sum_sq += val * val;
728        }
729        let mean = sum / n_strata as f64;
730        let var = (sum_sq / n_strata as f64 - mean * mean).max(0.0);
731        (
732            mean * vol * n_strata as f64 / n_strata as f64,
733            (var / n_strata as f64).sqrt(),
734        )
735    }
736
737    /// Latin Hypercube Sampling integration.
738    ///
739    /// Divides each dimension into n strata and samples one point per stratum.
740    pub fn integrate_lhs<F>(&mut self, f: F) -> (f64, f64)
741    where
742        F: Fn(&[f64]) -> f64,
743    {
744        let n = self.n_samples;
745        let d = self.dim;
746        // Generate permutations for each dimension
747        let mut perm: Vec<Vec<usize>> = (0..d)
748            .map(|_| {
749                let mut p: Vec<usize> = (0..n).collect();
750                // Fisher-Yates shuffle
751                for i in (1..n).rev() {
752                    let j = (self.rng.next_u64() as usize) % (i + 1);
753                    p.swap(i, j);
754                }
755                p
756            })
757            .collect();
758        let mut sum = 0.0;
759        let mut sum_sq = 0.0;
760        for i in 0..n {
761            let x: Vec<f64> = (0..d)
762                .map(|j| (perm[j][i] as f64 + self.rng.next_f64()) / n as f64)
763                .collect();
764            let val = f(&x);
765            sum += val;
766            sum_sq += val * val;
767            let _ = &mut perm; // keep alive
768        }
769        let mean = sum / n as f64;
770        let var = (sum_sq / n as f64 - mean * mean).max(0.0);
771        (mean, (var / n as f64).sqrt())
772    }
773
774    /// Quasi-Monte Carlo integration using a Halton sequence.
775    ///
776    /// Uses prime bases for each dimension.
777    pub fn integrate_halton<F>(&mut self, f: F) -> (f64, f64)
778    where
779        F: Fn(&[f64]) -> f64,
780    {
781        let primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
782        let n = self.n_samples;
783        let d = self.dim.min(primes.len());
784        let mut sum = 0.0;
785        let mut sum_sq = 0.0;
786        for i in 1..=n {
787            let x: Vec<f64> = (0..d).map(|j| halton_seq(i, primes[j])).collect();
788            let val = f(&x);
789            sum += val;
790            sum_sq += val * val;
791        }
792        let mean = sum / n as f64;
793        let var = (sum_sq / n as f64 - mean * mean).max(0.0);
794        (mean, (var / n as f64).sqrt())
795    }
796}
797
798/// Computes the i-th element of the Halton sequence in base `base`.
799fn halton_seq(mut i: usize, base: usize) -> f64 {
800    let mut f = 1.0_f64;
801    let mut r = 0.0_f64;
802    let b = base as f64;
803    while i > 0 {
804        f /= b;
805        r += f * (i % base) as f64;
806        i /= base;
807    }
808    r
809}
810
811// ─────────────────────────────────────────────────────────────────────────────
812// BootstrapCI — bootstrap confidence intervals
813// ─────────────────────────────────────────────────────────────────────────────
814
815/// Non-parametric bootstrap confidence interval estimator.
816///
817/// Supports percentile method and BCa (bias-corrected and accelerated) correction.
818pub struct BootstrapCI {
819    rng: LcgRng,
820    /// Number of bootstrap resamples.
821    pub n_boot: usize,
822}
823
824impl BootstrapCI {
825    /// Creates a new bootstrap CI estimator.
826    pub fn new(seed: u64, n_boot: usize) -> Self {
827        Self {
828            rng: LcgRng::new(seed),
829            n_boot,
830        }
831    }
832
833    /// Generates a bootstrap resample of `data`.
834    pub fn resample(&mut self, data: &[f64]) -> Vec<f64> {
835        let n = data.len();
836        (0..n)
837            .map(|_| data[self.rng.next_u64() as usize % n])
838            .collect()
839    }
840
841    /// Computes the percentile bootstrap CI for the mean at level `alpha`.
842    ///
843    /// Returns (lower, upper).
844    pub fn percentile_ci(&mut self, data: &[f64], alpha: f64) -> (f64, f64) {
845        let n_boot = self.n_boot;
846        let mut boot_means: Vec<f64> = (0..n_boot)
847            .map(|_| {
848                let resamp = self.resample(data);
849                resamp.iter().sum::<f64>() / resamp.len() as f64
850            })
851            .collect();
852        boot_means.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
853        let lo_idx = ((alpha / 2.0 * n_boot as f64) as usize).min(n_boot - 1);
854        let hi_idx = (((1.0 - alpha / 2.0) * n_boot as f64) as usize).min(n_boot - 1);
855        (boot_means[lo_idx], boot_means[hi_idx])
856    }
857
858    /// BCa bootstrap CI (bias-corrected and accelerated) for the mean.
859    ///
860    /// Returns (lower, upper).
861    pub fn bca_ci(&mut self, data: &[f64], alpha: f64) -> (f64, f64) {
862        let n = data.len();
863        let n_boot = self.n_boot;
864        let theta_hat = data.iter().sum::<f64>() / n as f64;
865
866        // Bootstrap distribution
867        let mut boot_stats: Vec<f64> = (0..n_boot)
868            .map(|_| {
869                let resamp = self.resample(data);
870                resamp.iter().sum::<f64>() / resamp.len() as f64
871            })
872            .collect();
873        boot_stats.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
874
875        // Bias correction z0
876        let count_less = boot_stats.iter().filter(|&&b| b < theta_hat).count();
877        let p0 = (count_less as f64 + 0.5) / (n_boot as f64 + 1.0);
878        let z0 = inverse_cdf(p0.clamp(0.001, 0.999));
879
880        // Acceleration a (jackknife-based)
881        let jack_means: Vec<f64> = (0..n)
882            .map(|i| {
883                let jack_sum: f64 = data
884                    .iter()
885                    .enumerate()
886                    .filter(|&(j, _)| j != i)
887                    .map(|(_, &x)| x)
888                    .sum();
889                jack_sum / (n - 1) as f64
890            })
891            .collect();
892        let jack_mean_mean = jack_means.iter().sum::<f64>() / n as f64;
893        let numer: f64 = jack_means
894            .iter()
895            .map(|&m| (jack_mean_mean - m).powi(3))
896            .sum();
897        let denom: f64 = 6.0
898            * jack_means
899                .iter()
900                .map(|&m| (jack_mean_mean - m).powi(2))
901                .sum::<f64>()
902                .powf(1.5);
903        let a = if denom.abs() < 1e-15 {
904            0.0
905        } else {
906            numer / denom
907        };
908
909        let adjust_quantile = |p: f64| -> f64 {
910            let z_p = inverse_cdf(p.clamp(0.001, 0.999));
911            let inner = z0 + (z0 + z_p) / (1.0 - a * (z0 + z_p));
912            // normal CDF approximation
913
914            0.5 * (1.0 + libm_erf(inner / 2.0_f64.sqrt()))
915        };
916
917        let p_lo = adjust_quantile(alpha / 2.0);
918        let p_hi = adjust_quantile(1.0 - alpha / 2.0);
919        let lo_idx = ((p_lo * n_boot as f64) as usize).min(n_boot - 1);
920        let hi_idx = ((p_hi * n_boot as f64) as usize).min(n_boot - 1);
921        (boot_stats[lo_idx], boot_stats[hi_idx])
922    }
923}
924
925/// Error function approximation (Abramowitz & Stegun 7.1.26).
926fn libm_erf(x: f64) -> f64 {
927    let t = 1.0 / (1.0 + 0.3275911 * x.abs());
928    let poly = t
929        * (0.254829592
930            + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
931    let sign = if x >= 0.0 { 1.0 } else { -1.0 };
932    sign * (1.0 - poly * (-x * x).exp())
933}
934
935// ─────────────────────────────────────────────────────────────────────────────
936// AnisotropicMC — directional distribution sampling
937// ─────────────────────────────────────────────────────────────────────────────
938
939/// Anisotropic Monte Carlo sampler for directional distributions.
940///
941/// Implements the von Mises-Fisher distribution for sampling on the unit sphere.
942pub struct AnisotropicMC {
943    rng: LcgRng,
944    /// Concentration parameter κ (κ=0 → uniform, κ→∞ → deterministic).
945    pub kappa: f64,
946    /// Mean direction (unit vector \[x, y, z\]).
947    pub mu: [f64; 3],
948}
949
950impl AnisotropicMC {
951    /// Creates a new anisotropic sampler.
952    pub fn new(seed: u64, kappa: f64, mu: [f64; 3]) -> Self {
953        Self {
954            rng: LcgRng::new(seed),
955            kappa,
956            mu,
957        }
958    }
959
960    /// Samples a unit vector from the von Mises-Fisher distribution.
961    ///
962    /// Returns a unit vector \[x, y, z\] concentrated around `mu`.
963    pub fn sample_vmf(&mut self) -> [f64; 3] {
964        let kappa = self.kappa;
965        // Sample cos(theta) from the marginal distribution
966        let cos_theta = if kappa < 1e-6 {
967            2.0 * self.rng.next_f64() - 1.0
968        } else {
969            let b = -kappa + (kappa * kappa + 1.0).sqrt();
970            let x0 = (1.0 - b) / (1.0 + b);
971            let c = kappa * x0 + 2.0 * (1.0 + b).ln() - (1.0 + x0).ln();
972            loop {
973                let z = self.rng.next_f64();
974                let w = (1.0 - (1.0 + b) * z) / (1.0 - (1.0 - b) * z);
975                let t = 2.0 * kappa * w / (1.0 + b * w);
976                let u = self.rng.next_f64();
977                if u.max(1e-15).ln() + c - t <= 0.0 {
978                    break w;
979                }
980            }
981        };
982        let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
983        let phi = 2.0 * PI * self.rng.next_f64();
984        // Sample in frame where mu = (0, 0, 1), then rotate
985        let v = [sin_theta * phi.cos(), sin_theta * phi.sin(), cos_theta];
986        self.rotate_to_mu(v)
987    }
988
989    fn rotate_to_mu(&self, v: [f64; 3]) -> [f64; 3] {
990        // Rotate (0,0,1) to mu using Rodrigues' rotation
991        let mu = self.mu;
992        let norm_mu = (mu[0] * mu[0] + mu[1] * mu[1] + mu[2] * mu[2])
993            .sqrt()
994            .max(1e-15);
995        let mu_hat = [mu[0] / norm_mu, mu[1] / norm_mu, mu[2] / norm_mu];
996        // If mu is already (0,0,1), return v directly
997        if (mu_hat[2] - 1.0).abs() < 1e-10 {
998            return v;
999        }
1000        if (mu_hat[2] + 1.0).abs() < 1e-10 {
1001            return [-v[0], -v[1], -v[2]];
1002        }
1003        // Axis of rotation: k = (0,0,1) x mu
1004        let k = [mu_hat[1], -mu_hat[0], 0.0];
1005        let k_norm = (k[0] * k[0] + k[1] * k[1]).sqrt().max(1e-15);
1006        let k_hat = [k[0] / k_norm, k[1] / k_norm, 0.0];
1007        let cos_a = mu_hat[2];
1008        let sin_a = (1.0 - cos_a * cos_a).sqrt();
1009        // Rodrigues: v_rot = v*cos_a + (k x v)*sin_a + k*(k·v)*(1-cos_a)
1010        let k_cross_v = [
1011            k_hat[1] * v[2] - k_hat[2] * v[1],
1012            k_hat[2] * v[0] - k_hat[0] * v[2],
1013            k_hat[0] * v[1] - k_hat[1] * v[0],
1014        ];
1015        let k_dot_v = k_hat[0] * v[0] + k_hat[1] * v[1] + k_hat[2] * v[2];
1016        [
1017            v[0] * cos_a + k_cross_v[0] * sin_a + k_hat[0] * k_dot_v * (1.0 - cos_a),
1018            v[1] * cos_a + k_cross_v[1] * sin_a + k_hat[1] * k_dot_v * (1.0 - cos_a),
1019            v[2] * cos_a + k_cross_v[2] * sin_a + k_hat[2] * k_dot_v * (1.0 - cos_a),
1020        ]
1021    }
1022}
1023
1024// ─────────────────────────────────────────────────────────────────────────────
1025// BrownianBridge
1026// ─────────────────────────────────────────────────────────────────────────────
1027
1028/// Brownian bridge and Ornstein-Uhlenbeck process sampler.
1029///
1030/// A Brownian bridge B(t) conditioned on B(0) = a, B(T) = b.
1031pub struct BrownianBridge {
1032    rng: LcgRng,
1033    /// Diffusion coefficient σ.
1034    pub sigma: f64,
1035}
1036
1037impl BrownianBridge {
1038    /// Creates a new Brownian bridge sampler.
1039    pub fn new(seed: u64, sigma: f64) -> Self {
1040        Self {
1041            rng: LcgRng::new(seed),
1042            sigma,
1043        }
1044    }
1045
1046    /// Samples a Brownian bridge path with `n` interior points.
1047    ///
1048    /// Returns n+2 values: \[a, interior..., b\] at times \[0, dt, ..., T\].
1049    pub fn brownian_bridge_sample(&mut self, n: usize, a: f64, b: f64, t_end: f64) -> Vec<f64> {
1050        let total = n + 2;
1051        let dt = t_end / (n + 1) as f64;
1052        let mut path = vec![0.0_f64; total];
1053        path[0] = a;
1054        path[total - 1] = b;
1055        // Generate free Brownian motion then pin endpoints
1056        let mut free = vec![a];
1057        for _ in 1..total {
1058            let prev = *free.last().expect("free path is non-empty");
1059            free.push(prev + self.sigma * dt.sqrt() * self.rng.next_normal());
1060        }
1061        // Condition on endpoint
1062        for i in 1..total - 1 {
1063            let t = i as f64 * dt;
1064            let t_rem = t_end - t;
1065            let mean_bridge = (t_rem * a + t * b) / t_end;
1066            let free_mean = a + (free[i] - a) * t / (total - 1) as f64;
1067            let _ = (free_mean, t_rem);
1068            path[i] = mean_bridge + (free[i] - free[0] - (free[total - 1] - free[0]) * t / t_end);
1069        }
1070        path
1071    }
1072
1073    /// Samples an Ornstein-Uhlenbeck path: dX = -theta*(X - mu)*dt + sigma*dW.
1074    ///
1075    /// Returns `n` samples at time step `dt`.
1076    pub fn ornstein_uhlenbeck(
1077        &mut self,
1078        x0: f64,
1079        theta: f64,
1080        mu: f64,
1081        dt: f64,
1082        n: usize,
1083    ) -> Vec<f64> {
1084        let sigma = self.sigma;
1085        let mut path = Vec::with_capacity(n);
1086        let mut x = x0;
1087        let exp_dt = (-theta * dt).exp();
1088        let std_step = sigma * ((1.0 - exp_dt * exp_dt) / (2.0 * theta)).sqrt().max(0.0);
1089        for _ in 0..n {
1090            x = mu + (x - mu) * exp_dt + std_step * self.rng.next_normal();
1091            path.push(x);
1092        }
1093        path
1094    }
1095}
1096
1097// ─────────────────────────────────────────────────────────────────────────────
1098// ParticleFilter — Sequential Monte Carlo
1099// ─────────────────────────────────────────────────────────────────────────────
1100
1101/// Sequential Monte Carlo particle filter.
1102///
1103/// Maintains a set of weighted particles representing the filtering distribution.
1104pub struct ParticleFilter {
1105    rng: LcgRng,
1106    /// Number of particles.
1107    pub n_particles: usize,
1108    /// Current particle positions.
1109    pub particles: Vec<f64>,
1110    /// Normalized particle weights.
1111    pub weights: Vec<f64>,
1112}
1113
1114impl ParticleFilter {
1115    /// Creates a new particle filter initialized with particles from N(mu0, sigma0).
1116    pub fn new(seed: u64, n_particles: usize, mu0: f64, sigma0: f64) -> Self {
1117        let mut rng = LcgRng::new(seed);
1118        let particles: Vec<f64> = (0..n_particles)
1119            .map(|_| mu0 + sigma0 * rng.next_normal())
1120            .collect();
1121        let weights = vec![1.0 / n_particles as f64; n_particles];
1122        Self {
1123            rng,
1124            n_particles,
1125            particles,
1126            weights,
1127        }
1128    }
1129
1130    /// Propagates particles through the dynamics model: x_new = f(x) + noise.
1131    pub fn predict<F>(&mut self, dynamics: F, noise_std: f64)
1132    where
1133        F: Fn(f64) -> f64,
1134    {
1135        for x in self.particles.iter_mut() {
1136            *x = dynamics(*x) + noise_std * self.rng.next_normal();
1137        }
1138    }
1139
1140    /// Updates weights with the likelihood of observation `y` given particles.
1141    ///
1142    /// `likelihood(x, y)` returns p(y | x).
1143    pub fn update<F>(&mut self, y: f64, likelihood: F)
1144    where
1145        F: Fn(f64, f64) -> f64,
1146    {
1147        for (x, w) in self.particles.iter().zip(self.weights.iter_mut()) {
1148            *w *= likelihood(*x, y);
1149        }
1150        self.normalize_weights();
1151    }
1152
1153    /// Normalizes weights to sum to 1.
1154    pub fn normalize_weights(&mut self) {
1155        let sum: f64 = self.weights.iter().sum();
1156        if sum > 1e-300 {
1157            for w in self.weights.iter_mut() {
1158                *w /= sum;
1159            }
1160        } else {
1161            let uniform = 1.0 / self.n_particles as f64;
1162            for w in self.weights.iter_mut() {
1163                *w = uniform;
1164            }
1165        }
1166    }
1167
1168    /// Systematic resampling: replaces degenerate particles.
1169    pub fn resample_systematic(&mut self) {
1170        let n = self.n_particles;
1171        let mut cumulative = vec![0.0_f64; n + 1];
1172        for i in 0..n {
1173            cumulative[i + 1] = cumulative[i] + self.weights[i];
1174        }
1175        let u = self.rng.next_f64() / n as f64;
1176        let mut new_particles = Vec::with_capacity(n);
1177        let mut j = 0;
1178        for i in 0..n {
1179            let threshold = u + i as f64 / n as f64;
1180            while j < n && cumulative[j + 1] < threshold {
1181                j += 1;
1182            }
1183            new_particles.push(self.particles[j.min(n - 1)]);
1184        }
1185        self.particles = new_particles;
1186        let uniform = 1.0 / n as f64;
1187        self.weights.fill(uniform);
1188    }
1189
1190    /// Multinomial resampling.
1191    pub fn resample_multinomial(&mut self) {
1192        let n = self.n_particles;
1193        let mut cumulative = vec![0.0_f64; n + 1];
1194        for i in 0..n {
1195            cumulative[i + 1] = cumulative[i] + self.weights[i];
1196        }
1197        let mut new_particles = Vec::with_capacity(n);
1198        for _ in 0..n {
1199            let u = self.rng.next_f64();
1200            let idx = cumulative
1201                .partition_point(|&c| c < u)
1202                .saturating_sub(1)
1203                .min(n - 1);
1204            new_particles.push(self.particles[idx]);
1205        }
1206        self.particles = new_particles;
1207        let uniform = 1.0 / n as f64;
1208        self.weights.fill(uniform);
1209    }
1210
1211    /// Weighted mean estimate of the current filtering distribution.
1212    pub fn estimate_mean(&self) -> f64 {
1213        self.particles
1214            .iter()
1215            .zip(self.weights.iter())
1216            .map(|(&x, &w)| x * w)
1217            .sum()
1218    }
1219
1220    /// Effective sample size of the particle filter.
1221    pub fn ess(&self) -> f64 {
1222        effective_sample_size(&self.weights)
1223    }
1224}
1225
1226// ─────────────────────────────────────────────────────────────────────────────
1227// Tests
1228// ─────────────────────────────────────────────────────────────────────────────
1229
1230#[cfg(test)]
1231mod tests {
1232    use super::*;
1233
1234    #[test]
1235    fn test_box_muller_finite() {
1236        let v = box_muller_normal(0.5, 0.25);
1237        assert!(v.is_finite());
1238    }
1239
1240    #[test]
1241    fn test_inverse_cdf_symmetry() {
1242        let x = inverse_cdf(0.975);
1243        assert!((x - 1.96).abs() < 0.01, "x={}", x);
1244        let y = inverse_cdf(0.025);
1245        assert!((y + 1.96).abs() < 0.01, "y={}", y);
1246    }
1247
1248    #[test]
1249    fn test_effective_sample_size_uniform() {
1250        let w = vec![0.25_f64; 4];
1251        let ess = effective_sample_size(&w);
1252        assert!((ess - 4.0).abs() < 1e-10);
1253    }
1254
1255    #[test]
1256    fn test_effective_sample_size_degenerate() {
1257        let mut w = vec![0.0_f64; 10];
1258        w[0] = 1.0;
1259        let ess = effective_sample_size(&w);
1260        assert!((ess - 1.0).abs() < 1e-10);
1261    }
1262
1263    #[test]
1264    fn test_autocorrelation_lag0() {
1265        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1266        let r0 = autocorrelation(&x, 0);
1267        assert!((r0 - 1.0).abs() < 1e-10);
1268    }
1269
1270    #[test]
1271    fn test_autocorrelation_constant() {
1272        let x = vec![3.0_f64; 10];
1273        let r = autocorrelation(&x, 1);
1274        // variance is 0, so result is 1.0
1275        assert!((r - 1.0).abs() < 1e-10);
1276    }
1277
1278    #[test]
1279    fn test_mc_sampler_uniform_range() {
1280        let mut s = McSampler::new(42);
1281        for _ in 0..100 {
1282            let x = s.uniform_range(2.0, 5.0);
1283            assert!((2.0..5.0).contains(&x));
1284        }
1285    }
1286
1287    #[test]
1288    fn test_mc_sampler_normal_stats() {
1289        let mut s = McSampler::new(123);
1290        let samples: Vec<f64> = (0..5000).map(|_| s.normal(0.0, 1.0)).collect();
1291        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
1292        let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
1293        assert!(mean.abs() < 0.1, "mean={}", mean);
1294        assert!((var - 1.0).abs() < 0.1, "var={}", var);
1295    }
1296
1297    #[test]
1298    fn test_mc_sampler_exponential_positive() {
1299        let mut s = McSampler::new(7);
1300        for _ in 0..100 {
1301            assert!(s.exponential(1.0) > 0.0);
1302        }
1303    }
1304
1305    #[test]
1306    fn test_mc_sampler_poisson_mean() {
1307        let mut s = McSampler::new(99);
1308        let lambda = 3.0;
1309        let samples: Vec<f64> = (0..2000).map(|_| s.poisson(lambda) as f64).collect();
1310        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
1311        assert!((mean - lambda).abs() < 0.2, "mean={}", mean);
1312    }
1313
1314    #[test]
1315    fn test_mc_sampler_beta_range() {
1316        let mut s = McSampler::new(55);
1317        for _ in 0..100 {
1318            let x = s.beta(2.0, 3.0);
1319            assert!((0.0..=1.0).contains(&x));
1320        }
1321    }
1322
1323    #[test]
1324    fn test_mc_sampler_gamma_positive() {
1325        let mut s = McSampler::new(77);
1326        for _ in 0..100 {
1327            assert!(s.gamma(2.0, 1.0) > 0.0);
1328        }
1329    }
1330
1331    #[test]
1332    fn test_estimator_mean_variance() {
1333        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1334        let est = Estimator::new(data);
1335        assert!((est.mean() - 3.0).abs() < 1e-12);
1336        assert!((est.variance() - 2.5).abs() < 1e-12);
1337    }
1338
1339    #[test]
1340    fn test_estimator_standard_error() {
1341        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1342        let est = Estimator::new(data);
1343        let se = est.standard_error();
1344        assert!(se > 0.0);
1345    }
1346
1347    #[test]
1348    fn test_estimator_confidence_interval() {
1349        let data: Vec<f64> = (0..100).map(|i| i as f64).collect();
1350        let est = Estimator::new(data);
1351        let (lo, hi) = est.confidence_interval(0.05);
1352        assert!(lo < hi);
1353        assert!(lo < 50.0 && hi > 50.0);
1354    }
1355
1356    #[test]
1357    fn test_estimator_median_odd() {
1358        let data = vec![3.0, 1.0, 2.0, 5.0, 4.0];
1359        let est = Estimator::new(data);
1360        assert!((est.median() - 3.0).abs() < 1e-12);
1361    }
1362
1363    #[test]
1364    fn test_metropolis_hastings_normal() {
1365        let mut mh = MetropolisHastings::new(42, 1.0, 500, 1);
1366        let samples = mh.sample(0.0, 1000, |x: f64| -0.5 * x * x);
1367        let est = Estimator::new(samples);
1368        assert!(est.mean().abs() < 0.2);
1369        assert!((est.variance() - 1.0).abs() < 0.3);
1370    }
1371
1372    #[test]
1373    fn test_hamiltonian_mc_normal() {
1374        let mut hmc = HamiltonianMC::new(42, 0.3, 10, 200);
1375        let samples = hmc.sample(0.0, 500, |x: f64| -0.5 * x * x, |x: f64| x);
1376        let est = Estimator::new(samples);
1377        assert!(est.mean().abs() < 0.3);
1378    }
1379
1380    #[test]
1381    fn test_mc_integral_plain_pi() {
1382        // Estimate pi/4 by integrating indicator of unit circle in [0,1]^2
1383        let mut mc = MonteCarloIntegral::new(42, 10000, 2);
1384        let (est, _se) = mc.integrate_plain(|x: &[f64]| {
1385            if x[0] * x[0] + x[1] * x[1] <= 1.0 {
1386                1.0
1387            } else {
1388                0.0
1389            }
1390        });
1391        assert!((est - PI / 4.0).abs() < 0.05, "est={}", est);
1392    }
1393
1394    #[test]
1395    fn test_mc_integral_halton_constant() {
1396        let mut mc = MonteCarloIntegral::new(1, 1000, 1);
1397        let (est, _se) = mc.integrate_halton(|_: &[f64]| 1.0);
1398        assert!((est - 1.0).abs() < 0.01);
1399    }
1400
1401    #[test]
1402    fn test_bootstrap_ci_contains_mean() {
1403        let data: Vec<f64> = (0..50).map(|i| i as f64).collect();
1404        let mut boot = BootstrapCI::new(42, 500);
1405        let (lo, hi) = boot.percentile_ci(&data, 0.05);
1406        assert!(lo < 24.5 && hi > 24.5);
1407    }
1408
1409    #[test]
1410    fn test_brownian_bridge_endpoints() {
1411        let mut bb = BrownianBridge::new(42, 0.1);
1412        let path = bb.brownian_bridge_sample(10, 0.0, 1.0, 1.0);
1413        assert_eq!(path.len(), 12);
1414        assert!((path[0] - 0.0).abs() < 1e-12);
1415        assert!((path[11] - 1.0).abs() < 1e-12);
1416    }
1417
1418    #[test]
1419    fn test_ou_process_mean_reversion() {
1420        let mut bb = BrownianBridge::new(42, 0.1);
1421        let path = bb.ornstein_uhlenbeck(5.0, 1.0, 0.0, 0.1, 100);
1422        // After 100 steps, should be closer to 0 than initial 5.0
1423        let final_val = path.last().unwrap().abs();
1424        assert!(final_val < 5.0, "final_val={}", final_val);
1425    }
1426
1427    #[test]
1428    fn test_particle_filter_mean_estimate() {
1429        let mut pf = ParticleFilter::new(42, 200, 0.0, 1.0);
1430        // Observe y=2.0; likelihood = N(x, 0.5)
1431        pf.update(2.0, |x: f64, y: f64| {
1432            let d = (x - y) / 0.5;
1433            (-0.5 * d * d).exp()
1434        });
1435        let mean = pf.estimate_mean();
1436        // Should shift toward observation
1437        assert!(mean > 0.0);
1438    }
1439
1440    #[test]
1441    fn test_particle_filter_resample_systematic() {
1442        let mut pf = ParticleFilter::new(42, 50, 0.0, 1.0);
1443        pf.update(1.0, |x: f64, y: f64| (-(x - y).powi(2)).exp());
1444        let ess_before = pf.ess();
1445        pf.resample_systematic();
1446        let ess_after = pf.ess();
1447        // After resampling, weights are uniform so ESS = N
1448        assert!(ess_after >= ess_before || ess_after > 1.0);
1449    }
1450
1451    #[test]
1452    fn test_anisotropic_mc_unit_sphere() {
1453        let mut amc = AnisotropicMC::new(42, 2.0, [0.0, 0.0, 1.0]);
1454        for _ in 0..20 {
1455            let v = amc.sample_vmf();
1456            let norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1457            assert!((norm - 1.0).abs() < 1e-10, "norm={}", norm);
1458        }
1459    }
1460
1461    #[test]
1462    fn test_mc_integral_lhs() {
1463        let mut mc = MonteCarloIntegral::new(42, 100, 1);
1464        let (est, _) = mc.integrate_lhs(|x: &[f64]| x[0]);
1465        // E[X] = 0.5 for X ~ U(0,1)
1466        assert!((est - 0.5).abs() < 0.1, "est={}", est);
1467    }
1468
1469    #[test]
1470    fn test_halton_sequence() {
1471        let h = halton_seq(1, 2);
1472        assert!((h - 0.5).abs() < 1e-12);
1473        let h2 = halton_seq(2, 2);
1474        assert!((h2 - 0.25).abs() < 1e-12);
1475    }
1476
1477    #[test]
1478    fn test_importance_sampler_estimate() {
1479        let mut is = ImportanceSampler::new(42, 1.0);
1480        // Estimate E_N(0,1)[x^2] = 1 using proposal N(0,1) with target N(0,1)
1481        let est = is.estimate(
1482            1000,
1483            |x: f64| x * x,
1484            |_x: f64| 0.0, // log(target/proposal) = 0 when equal
1485        );
1486        assert!((est - 1.0).abs() < 0.2, "est={}", est);
1487    }
1488}