use super::*;
use crate::matrix::rand_util::collect_f32_seeded;
struct Lcg(u64);
impl Lcg {
fn next_f32(&mut self) -> f32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
((self.0 >> 33) as f32) / ((1u64 << 31) as f32) - 0.5
}
}
#[test]
fn iid_chain_has_ess_near_n() {
let mut rng = Lcg(42);
let x: Vec<f32> = (0..4000).map(|_| rng.next_f32()).collect();
let e = ess(&x);
assert!(
e > 0.6 * x.len() as f32,
"iid chain should keep most of its draws, got ess={e} of {}",
x.len()
);
}
#[test]
fn autocorrelated_chain_has_ess_well_below_n() {
let mut rng = Lcg(7);
let rho = 0.9f32;
let mut v = 0.0f32;
let mut x = Vec::with_capacity(4000);
for _ in 0..4000 {
v = rho * v + rng.next_f32();
x.push(v);
}
let e = ess(&x);
let n = x.len() as f32;
assert!(
e < n / 5.0,
"a strongly autocorrelated chain must be discounted, got ess={e} of {n}"
);
assert!(e >= 1.0, "ess stays positive, got {e}");
}
#[test]
fn constant_chain_does_not_divide_by_zero() {
let e = ess(&vec![1.0f32; 500]);
assert_eq!(e, 500.0);
let short = ess(&[1.0, 2.0]);
assert_eq!(short, 2.0, "too short to estimate ⇒ n");
}
#[test]
fn mcse_is_nonzero_at_p_zero() {
let se = mcse_proportion(0.0, 1000.0);
assert!(se > 0.0, "p=0 must still carry MC error, got {se}");
assert!(
se < 0.002,
"…but it should be small at ess=1000, got {se}" );
let se_border = mcse_proportion(0.1, 1000.0);
assert!(
se_border > 0.008 && se_border < 0.011,
"lfsr=0.1 at ess=1000 ⇒ mcse≈0.0095, got {se_border}"
);
assert!(mcse_proportion(0.1, 100.0) > se_border);
}
#[test]
fn a_stationary_chain_has_rhat_near_one() {
let x = normal_chain(11, 800, 0.0, 1.0);
let r = split_rhat(&x);
assert!(
(r - 1.0).abs() < 0.05,
"iid draws should give R̂ ≈ 1, got {r}"
);
}
#[test]
fn a_drifting_chain_is_caught() {
let x: Vec<f32> = (0..800).map(|i| i as f32 * 0.01).collect();
let r = split_rhat(&x);
assert!(
r > 1.1,
"a linear drift must not pass as converged, got R̂ {r}"
);
}
#[test]
fn a_constant_chain_reports_one_not_a_division_by_zero() {
let x = vec![0.7f32; 400];
let r = split_rhat(&x);
assert_eq!(r, 1.0, "a pinned chain should read 1.0, got {r}");
}
#[test]
fn constant_but_disagreeing_segments_are_not_converged() {
let mut x = vec![0.0f32; 200];
x.extend(vec![5.0f32; 200]);
assert!(
split_rhat(&x) > 1.1,
"two constant halves at different levels are not converged"
);
}
#[test]
fn a_chain_too_short_to_split_makes_no_claim() {
assert_eq!(split_rhat(&[1.0, 2.0, 3.0]), 1.0);
assert_eq!(split_rhat(&[]), 1.0);
}
fn normal_chain(seed: u64, n: usize, mean: f32, sd: f32) -> Vec<f32> {
collect_f32_seeded(n, rand_distr::Normal::new(mean, sd).unwrap(), seed)
}
#[test]
fn chains_sampling_the_same_target_have_rhat_near_one() {
let chains: Vec<Vec<f32>> = (0..4)
.map(|s| normal_chain(100 + s, 500, 0.0, 1.0))
.collect();
let r = split_rhat_chains(&chains);
assert!(
(r - 1.0).abs() < 0.05,
"same target should give R̂ ≈ 1, got {r}"
);
}
#[test]
fn chains_stuck_in_different_modes_are_caught() {
let a = normal_chain(1, 400, 0.0, 0.07);
let b = normal_chain(2, 400, 2.0, 0.07);
assert!(split_rhat(&a) < 1.05 && split_rhat(&b) < 1.05);
let r = split_rhat_chains(&[&a, &b]);
assert!(r > 2.0, "chains at means 0 and 2 must not pass, got R̂ {r}");
}
#[test]
fn chains_drifting_together_are_caught_by_the_split() {
let drift = |seed| {
let mut x = normal_chain(seed, 200, 0.0, 0.1);
x.extend(normal_chain(seed + 50, 200, 2.0, 0.1));
x
};
let (a, b) = (drift(3), drift(4));
let whole = gelman_rubin(&[&a, &b]);
assert!(whole < 1.01, "whole-chain R̂ misses the drift ({whole})");
let r = split_rhat_chains(&[&a, &b]);
assert!(r > 1.1, "the split must catch a shared drift, got R̂ {r}");
}
#[test]
fn chains_are_truncated_to_the_shortest() {
let a = normal_chain(5, 300, 0.0, 1.0);
let mut b = normal_chain(6, 300, 0.0, 1.0);
let truncated = split_rhat_chains(&[&a, &b]);
b.extend(vec![100.0f32; 500]); assert_eq!(split_rhat_chains(&[&a, &b]), truncated);
}
#[test]
fn degenerate_chains_follow_split_rhat() {
let (a, b) = (vec![0.5f32; 100], vec![0.5f32; 100]);
assert_eq!(split_rhat_chains(&[&a, &b]), 1.0);
let c = vec![1.5f32; 100];
assert_eq!(split_rhat_chains(&[&a, &c]), f32::INFINITY);
assert_eq!(split_rhat_chains::<&[f32]>(&[]), 1.0);
assert_eq!(split_rhat_chains(&[&[1.0, 2.0, 3.0][..], &a]), 1.0);
}