use super::*;
use crate::mcmc::engine::elliptical_slice_step;
use nalgebra::DVector;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use rand_distr::{Distribution, StandardNormal};
fn make_ll(mu: Vec<f32>, s: f32) -> impl Fn(usize, f32) -> f32 {
move |i, x| {
let z = (x - mu[i]) / s;
-0.5 * z * z
}
}
#[test]
fn the_batch_is_n_scalar_transitions_bit_for_bit() {
const N: usize = 64;
let seeds: Vec<u64> = (0..N as u64).map(|i| 0xC0FF_EE00 ^ (i * 7 + 1)).collect();
let mu: Vec<f32> = (0..N).map(|i| (i % 9) as f32 * 0.3 - 1.2).collect();
let ll = make_ll(mu.clone(), 0.8);
let cur: Vec<f32> = (0..N).map(|i| (i % 5) as f32 * 0.2 - 0.4).collect();
let nu: Vec<f32> = (0..N).map(|i| (i % 7) as f32 * 0.25 - 0.75).collect();
let cur_ll: Vec<f32> = (0..N).map(|i| ll(i, cur[i])).collect();
let mut rngs: Vec<SmallRng> = seeds.iter().map(|&s| SmallRng::seed_from_u64(s)).collect();
let step = elliptical_slice_batch(&cur, &nu, &cur_ll, &mut rngs, &mut |x, active, out| {
for (slot, &i) in active.iter().enumerate() {
out[slot] = ll(i as usize, x[slot]);
}
});
for i in 0..N {
let mut rng = SmallRng::seed_from_u64(seeds[i]);
let c = DVector::from_vec(vec![cur[i]]);
let p = DVector::from_vec(vec![nu[i]]);
let (v, l) =
elliptical_slice_step(&c, &p, &|x: &DVector<f32>| ll(i, x[0]), cur_ll[i], &mut rng);
assert_eq!(
step.value[i], v[0],
"item {i}: batch value {} vs scalar {}",
step.value[i], v[0]
);
assert_eq!(
step.lnpdf[i], l,
"item {i}: batch lnpdf disagrees with scalar"
);
}
}
#[test]
fn grouping_into_batches_changes_nothing() {
const N: usize = 40;
let seeds: Vec<u64> = (0..N as u64).map(|i| 0xABCD_0000 ^ (i * 13 + 5)).collect();
let mu: Vec<f32> = (0..N).map(|i| (i % 11) as f32 * 0.2 - 1.0).collect();
let ll = make_ll(mu, 0.7);
let cur: Vec<f32> = (0..N).map(|i| (i % 6) as f32 * 0.15 - 0.4).collect();
let nu: Vec<f32> = (0..N).map(|i| (i % 8) as f32 * 0.2 - 0.7).collect();
let cur_ll: Vec<f32> = (0..N).map(|i| ll(i, cur[i])).collect();
let one = {
let mut rngs: Vec<SmallRng> = seeds.iter().map(|&s| SmallRng::seed_from_u64(s)).collect();
elliptical_slice_batch(&cur, &nu, &cur_ll, &mut rngs, &mut |x, active, out| {
for (slot, &i) in active.iter().enumerate() {
out[slot] = ll(i as usize, x[slot]);
}
})
};
let mut split_value = vec![0.0f32; N];
let mut split_lnpdf = vec![0.0f32; N];
for chunk in 0..4 {
let lo = chunk * 10;
let hi = lo + 10;
let mut rngs: Vec<SmallRng> = seeds[lo..hi]
.iter()
.map(|&s| SmallRng::seed_from_u64(s))
.collect();
let part = elliptical_slice_batch(
&cur[lo..hi],
&nu[lo..hi],
&cur_ll[lo..hi],
&mut rngs,
&mut |x, active, out| {
for (slot, &i) in active.iter().enumerate() {
out[slot] = ll(lo + i as usize, x[slot]);
}
},
);
split_value[lo..hi].copy_from_slice(&part.value);
split_lnpdf[lo..hi].copy_from_slice(&part.lnpdf);
}
assert_eq!(one.value, split_value, "values depend on batch grouping");
assert_eq!(one.lnpdf, split_lnpdf, "lnpdf depends on batch grouping");
}
#[test]
fn it_targets_the_analytic_posterior() {
const N: usize = 256;
const SWEEPS: usize = 600;
const BURN: usize = 100;
let mu = vec![2.0f32; N];
let ll = make_ll(mu, 1.0);
let mut rngs: Vec<SmallRng> = (0..N as u64)
.map(|i| SmallRng::seed_from_u64(0x5EED ^ (i * 31 + 3)))
.collect();
let mut draw_rng = SmallRng::seed_from_u64(99);
let mut cur = vec![0.0f32; N];
let mut cur_ll: Vec<f32> = (0..N).map(|i| ll(i, cur[i])).collect();
let mut sum = 0.0f64;
let mut sumsq = 0.0f64;
let mut kept = 0usize;
for sweep in 0..SWEEPS {
let nu: Vec<f32> = (0..N)
.map(|_| {
let g: f64 = StandardNormal.sample(&mut draw_rng);
g as f32
})
.collect();
let step = elliptical_slice_batch(&cur, &nu, &cur_ll, &mut rngs, &mut |x, active, out| {
for (slot, &i) in active.iter().enumerate() {
out[slot] = ll(i as usize, x[slot]);
}
});
cur = step.value;
cur_ll = step.lnpdf;
if sweep >= BURN {
for &v in &cur {
sum += f64::from(v);
sumsq += f64::from(v) * f64::from(v);
}
kept += N;
}
}
let mean = sum / kept as f64;
let var = sumsq / kept as f64 - mean * mean;
assert!(
(mean - 1.0).abs() < 0.05,
"posterior mean {mean:.4} should be 1.0 — sampling the prior would give 0.0"
);
assert!(
(var - 0.5).abs() < 0.05,
"posterior variance {var:.4} should be 0.5 — sampling the prior would give 1.0"
);
}
#[test]
fn an_unsatisfiable_slice_falls_back_and_is_counted() {
const N: usize = 16;
let cur = vec![0.3f32; N];
let nu = vec![1.0f32; N];
let cur_ll = vec![0.0f32; N];
let mut rngs: Vec<SmallRng> = (0..N as u64)
.map(|i| SmallRng::seed_from_u64(7 ^ i))
.collect();
let step = elliptical_slice_batch(&cur, &nu, &cur_ll, &mut rngs, &mut |_x, _active, out| {
out.fill(f32::NEG_INFINITY);
});
assert_eq!(step.fallbacks, N, "every item should have fallen back");
assert_eq!(step.value, cur, "a fallback must leave the value untouched");
assert_eq!(
step.lnpdf, cur_ll,
"a fallback must leave the lnpdf untouched"
);
assert!(
step.rounds <= MAX_BRACKET_ITERS,
"rounds {} exceeded the cap",
step.rounds
);
}
#[test]
fn an_easy_slice_retires_the_active_set_fast() {
const N: usize = 512;
let ll = make_ll(vec![0.0f32; N], 4.0); let cur = vec![0.1f32; N];
let nu: Vec<f32> = (0..N).map(|i| (i % 3) as f32 * 0.1 - 0.1).collect();
let cur_ll: Vec<f32> = (0..N).map(|i| ll(i, cur[i])).collect();
let mut rngs: Vec<SmallRng> = (0..N as u64)
.map(|i| SmallRng::seed_from_u64(0x1234 ^ (i * 3 + 1)))
.collect();
let step = elliptical_slice_batch(&cur, &nu, &cur_ll, &mut rngs, &mut |x, active, out| {
for (slot, &i) in active.iter().enumerate() {
out[slot] = ll(i as usize, x[slot]);
}
});
assert_eq!(step.fallbacks, 0, "a broad likelihood should not stall");
assert!(
step.rounds < 20,
"took {} rounds for {N} items — the active set is not decaying, so the \
batch is doing n scalar walks with extra bookkeeping",
step.rounds
);
}