use crate::core::address::Address;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DiminishingAdaptation {
pub scales: HashMap<Address, (f64, f64)>,
pub accept_counts: HashMap<Address, usize>,
pub total_counts: HashMap<Address, usize>,
pub target_rate: f64,
pub gamma: f64,
}
impl DiminishingAdaptation {
pub fn new(target_rate: f64, gamma: f64) -> Self {
assert!(target_rate > 0.0 && target_rate < 1.0);
assert!(gamma > 0.5 && gamma <= 1.0);
Self {
scales: HashMap::new(),
accept_counts: HashMap::new(),
total_counts: HashMap::new(),
target_rate,
gamma,
}
}
pub fn get_scale(&mut self, addr: &Address) -> f64 {
if let Some(&(scale, _)) = self.scales.get(addr) {
scale
} else {
self.scales.insert(addr.clone(), (1.0, 0.0));
1.0
}
}
pub fn update(&mut self, addr: &Address, accepted: bool) {
let total_count = match self.total_counts.get_mut(addr) {
Some(t) => {
*t += 1;
*t
}
None => {
self.total_counts.insert(addr.clone(), 1);
1
}
};
if accepted {
match self.accept_counts.get_mut(addr) {
Some(a) => *a += 1,
None => {
self.accept_counts.insert(addr.clone(), 1);
}
}
}
let accept_count = *self.accept_counts.get(addr).unwrap_or(&0);
if total_count < 10 {
return; }
let accept_rate = accept_count as f64 / total_count as f64;
let step_size = 1.0 / (total_count as f64).powf(self.gamma);
let entry = match self.scales.get_mut(addr) {
Some(e) => e,
None => {
self.scales.insert(addr.clone(), (1.0, 0.0));
self.scales
.get_mut(addr)
.expect("just inserted the scale entry")
}
};
let (ref mut scale, ref mut log_scale) = *entry;
*log_scale += step_size * (accept_rate - self.target_rate);
let new_scale = log_scale.exp();
*scale = if new_scale.is_finite() && new_scale > 0.0 {
new_scale.clamp(0.001, 100.0)
} else {
1.0 };
if *scale == 1.0 {
*log_scale = 0.0; } else {
*log_scale = scale.ln(); }
}
pub fn should_continue_adaptation(&self, min_updates: usize) -> bool {
self.total_counts.values().any(|&count| count < min_updates)
}
pub fn get_stats(&self) -> Vec<(Address, f64, f64, usize)> {
self.scales
.iter()
.map(|(addr, &(scale, _log_scale))| {
let accepts = *self.accept_counts.get(addr).unwrap_or(&0);
let total = *self.total_counts.get(addr).unwrap_or(&0);
let rate = if total > 0 {
accepts as f64 / total as f64
} else {
0.0
};
(addr.clone(), scale, rate, total)
})
.collect()
}
}
pub fn effective_sample_size_mcmc(samples: &[f64]) -> f64 {
let n = samples.len();
if n < 4 {
return n as f64; }
ess_from_chains(&[samples])
}
pub fn effective_sample_size_multichain(chains: &[Vec<f64>]) -> f64 {
if chains.is_empty() {
return 0.0;
}
let refs: Vec<&[f64]> = chains.iter().map(|c| c.as_slice()).collect();
let n = refs[0].len();
if n < 4 || refs.iter().any(|c| c.len() != n) {
return chains.iter().map(|c| c.len()).sum::<usize>().max(1) as f64;
}
ess_from_chains(&refs)
}
fn autocovariances(x: &[f64], max_lag: usize) -> Vec<f64> {
let n = x.len();
let mean = x.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = x.iter().map(|&v| v - mean).collect();
let mut acov = Vec::with_capacity(max_lag + 1);
for lag in 0..=max_lag {
let mut s = 0.0;
for i in 0..(n - lag) {
s += centered[i] * centered[i + lag];
}
acov.push(s / n as f64);
}
acov
}
fn ess_from_chains(chains: &[&[f64]]) -> f64 {
let m = chains.len();
if m == 0 {
return 0.0;
}
let n = chains[0].len();
if n < 4 || chains.iter().any(|c| c.len() != n) {
return chains.iter().map(|c| c.len()).sum::<usize>().max(1) as f64;
}
let max_lag = (n - 1).min(2048);
let acovs: Vec<Vec<f64>> = chains.iter().map(|c| autocovariances(c, max_lag)).collect();
let nf = n as f64;
let mf = m as f64;
let chain_means: Vec<f64> = chains.iter().map(|c| c.iter().sum::<f64>() / nf).collect();
let chain_vars: Vec<f64> = acovs.iter().map(|a| a[0] * nf / (nf - 1.0)).collect();
let mean_var = chain_vars.iter().sum::<f64>() / mf;
if mean_var <= 0.0 {
return (m * n) as f64;
}
let mut var_plus = mean_var * (nf - 1.0) / nf;
if m > 1 {
let overall = chain_means.iter().sum::<f64>() / mf;
let between = chain_means
.iter()
.map(|&mu| (mu - overall).powi(2))
.sum::<f64>()
/ (mf - 1.0);
var_plus += between;
}
let rho = |t: usize| -> f64 {
let acov_t = acovs.iter().map(|a| a[t]).sum::<f64>() / mf;
1.0 - (mean_var - acov_t) / var_plus
};
let mut rho_hat = vec![0.0f64; max_lag + 1];
rho_hat[0] = 1.0;
if max_lag >= 1 {
rho_hat[1] = rho(1);
}
let mut t = 1usize;
let mut max_t = 1usize.min(max_lag);
while t + 2 <= max_lag {
let rho_even = rho(t + 1);
let rho_odd = rho(t + 2);
if rho_even + rho_odd < 0.0 {
break;
}
rho_hat[t + 1] = rho_even;
rho_hat[t + 2] = rho_odd;
max_t = t + 2;
t += 2;
}
let mut k = 1usize;
while k + 2 <= max_t {
let prev = rho_hat[k - 1] + rho_hat[k];
let cur = rho_hat[k + 1] + rho_hat[k + 2];
if cur > prev {
let avg = prev / 2.0;
rho_hat[k + 1] = avg;
rho_hat[k + 2] = avg;
}
k += 2;
}
let sum_rho: f64 = rho_hat[0..=max_t].iter().sum();
let tau = (-1.0 + 2.0 * sum_rho).max(1.0);
(m * n) as f64 / tau
}
pub fn geweke_diagnostic(chain: &[f64]) -> f64 {
let n = chain.len();
if n < 20 {
return f64::NAN; }
let first_end = n / 10;
let last_start = n / 2;
let first_part = &chain[0..first_end];
let last_part = &chain[last_start..];
if first_part.len() < 2 || last_part.len() < 2 {
return f64::NAN;
}
let mean1 = first_part.iter().sum::<f64>() / first_part.len() as f64;
let mean2 = last_part.iter().sum::<f64>() / last_part.len() as f64;
let varmean1 = spectral_variance_of_mean(first_part);
let varmean2 = spectral_variance_of_mean(last_part);
let se = (varmean1 + varmean2).sqrt();
if se == 0.0 {
return 0.0; }
(mean1 - mean2) / se
}
fn spectral_variance_of_mean(seg: &[f64]) -> f64 {
let n = seg.len();
if n < 2 {
return 0.0;
}
let mean = seg.iter().sum::<f64>() / n as f64;
let s2 = seg.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n as f64 - 1.0);
if s2 == 0.0 {
return 0.0;
}
let max_lag = (n - 1).min(1024);
let acov = autocovariances(seg, max_lag);
let var0 = acov[0];
if var0 <= 0.0 {
return 0.0;
}
let mut tau = 1.0;
for &cov in acov.iter().skip(1) {
let rho_k = cov / var0;
if rho_k <= 0.0 {
break;
}
tau += 2.0 * rho_k;
}
s2 * tau / n as f64
}
#[cfg(test)]
mod mcmc_tests {
use super::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
#[test]
fn test_diminishing_adaptation() {
let mut adapter = DiminishingAdaptation::new(0.44, 0.7);
let addr = Address::new("test");
assert_eq!(adapter.get_scale(&addr), 1.0);
for _ in 0..500 {
adapter.update(&addr, true);
}
assert!(adapter.get_scale(&addr) > 1.0);
for _ in 0..500 {
adapter.update(&addr, false);
}
let final_scale = adapter.get_scale(&addr);
assert!(final_scale > 0.0 && final_scale.is_finite()); }
#[test]
fn test_effective_sample_size() {
let random: Vec<f64> = (0..100)
.map(|i| (i as f64).sin() * (i as f64).cos())
.collect();
let ess = effective_sample_size_mcmc(&random);
assert!(ess > 1.0 && ess <= 100.0);
let correlated: Vec<f64> = (0..100).map(|i| (i / 10) as f64).collect();
let ess_corr = effective_sample_size_mcmc(&correlated);
assert!(ess_corr > 0.0 && ess_corr <= 100.0); }
#[test]
fn ess_is_scale_invariant() {
let mut rng = StdRng::seed_from_u64(20260710);
let phi = 0.6;
let n = 3000;
let mut x = 0.0;
let mut series = Vec::with_capacity(n);
for _ in 0..n {
let z: f64 = {
let u1: f64 = rng.gen::<f64>().max(1e-12);
let u2: f64 = rng.gen();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
};
x = phi * x + z;
series.push(x);
}
let ess_base = effective_sample_size_mcmc(&series);
let scaled: Vec<f64> = series.iter().map(|&v| v * 1000.0).collect();
let ess_scaled = effective_sample_size_mcmc(&scaled);
let rel = (ess_base - ess_scaled).abs() / ess_base;
assert!(
rel < 1e-9,
"ESS not scale-invariant: base={ess_base}, scaled={ess_scaled}"
);
}
#[test]
fn ess_matches_ar1_known_answer() {
let mut rng = StdRng::seed_from_u64(424242);
let phi = 0.9_f64;
let n = 8000;
let mut x = 0.0;
let mut series = Vec::with_capacity(n);
for _ in 0..n {
let u1: f64 = rng.gen::<f64>().max(1e-12);
let u2: f64 = rng.gen();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
x = phi * x + z;
series.push(x);
}
let ess = effective_sample_size_mcmc(&series);
let ratio = ess / n as f64;
let expected = (1.0 - phi) / (1.0 + phi); let rel = (ratio - expected).abs() / expected;
assert!(
rel < 0.15,
"AR(1) ESS/n = {ratio:.4}, expected ≈ {expected:.4} (rel err {rel:.3})"
);
}
#[test]
fn geweke_stationary_is_small() {
let mut rng = StdRng::seed_from_u64(9001);
let phi = 0.9_f64;
let n = 6000;
let mut x = 0.0;
let mut series = Vec::with_capacity(n);
for _ in 0..n {
let u1: f64 = rng.gen::<f64>().max(1e-12);
let u2: f64 = rng.gen();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
x = phi * x + z;
series.push(x);
}
let z = geweke_diagnostic(&series);
assert!(
z.abs() < 3.0,
"stationary Geweke |z| = {z:.3} should be < 3"
);
}
#[test]
fn geweke_drift_is_flagged() {
let mut rng = StdRng::seed_from_u64(9002);
let n = 6000;
let mut series = Vec::with_capacity(n);
for i in 0..n {
let u1: f64 = rng.gen::<f64>().max(1e-12);
let u2: f64 = rng.gen();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
series.push(i as f64 * 0.01 + 0.5 * z);
}
let z = geweke_diagnostic(&series);
assert!(z.abs() > 4.0, "drifting Geweke |z| = {z:.3} should be > 4");
}
}