use crate::core::address::Address;
use crate::inference::mcmc_utils::{effective_sample_size_mcmc, effective_sample_size_multichain};
use crate::runtime::trace::Trace;
use std::collections::HashMap;
pub fn extract_f64_values(traces: &[Trace], addr: &Address) -> Vec<f64> {
traces.iter().filter_map(|t| t.get_f64(addr)).collect()
}
pub fn extract_bool_values(traces: &[Trace], addr: &Address) -> Vec<bool> {
traces.iter().filter_map(|t| t.get_bool(addr)).collect()
}
pub fn extract_u64_values(traces: &[Trace], addr: &Address) -> Vec<u64> {
traces.iter().filter_map(|t| t.get_u64(addr)).collect()
}
pub fn extract_usize_values(traces: &[Trace], addr: &Address) -> Vec<usize> {
traces.iter().filter_map(|t| t.get_usize(addr)).collect()
}
pub fn extract_i64_values(traces: &[Trace], addr: &Address) -> Vec<i64> {
traces.iter().filter_map(|t| t.get_i64(addr)).collect()
}
pub trait Diagnostics<T> {
fn extract_values(traces: &[Trace], addr: &Address) -> Vec<T>;
fn r_hat(chains: &[Vec<Trace>], addr: &Address) -> Option<f64>;
fn effective_sample_size(values: &[T]) -> Option<f64>;
}
impl Diagnostics<f64> for f64 {
fn extract_values(traces: &[Trace], addr: &Address) -> Vec<f64> {
extract_f64_values(traces, addr)
}
fn r_hat(chains: &[Vec<Trace>], addr: &Address) -> Option<f64> {
let r_hat_val = r_hat_f64(chains, addr);
if r_hat_val.is_finite() {
Some(r_hat_val)
} else {
None
}
}
fn effective_sample_size(values: &[f64]) -> Option<f64> {
if values.len() < 4 {
return Some(values.len() as f64);
}
Some(effective_sample_size(values))
}
}
impl Diagnostics<bool> for bool {
fn extract_values(traces: &[Trace], addr: &Address) -> Vec<bool> {
extract_bool_values(traces, addr)
}
fn r_hat(_chains: &[Vec<Trace>], _addr: &Address) -> Option<f64> {
None
}
fn effective_sample_size(_values: &[bool]) -> Option<f64> {
None
}
}
impl Diagnostics<u64> for u64 {
fn extract_values(traces: &[Trace], addr: &Address) -> Vec<u64> {
extract_u64_values(traces, addr)
}
fn r_hat(chains: &[Vec<Trace>], addr: &Address) -> Option<f64> {
let f64_chains: Vec<Vec<f64>> = chains
.iter()
.map(|chain| {
extract_u64_values(chain, addr)
.into_iter()
.map(|x| x as f64)
.collect()
})
.collect();
if f64_chains.iter().any(|v| v.is_empty()) {
return None;
}
let r_hat_val = split_r_hat_from_f64_chains(&f64_chains);
if r_hat_val.is_finite() {
Some(r_hat_val)
} else {
None
}
}
fn effective_sample_size(values: &[u64]) -> Option<f64> {
if values.len() < 4 {
return Some(values.len() as f64);
}
let f64_values: Vec<f64> = values.iter().map(|&x| x as f64).collect();
Some(effective_sample_size(&f64_values))
}
}
impl Diagnostics<usize> for usize {
fn extract_values(traces: &[Trace], addr: &Address) -> Vec<usize> {
extract_usize_values(traces, addr)
}
fn r_hat(_chains: &[Vec<Trace>], _addr: &Address) -> Option<f64> {
None
}
fn effective_sample_size(_values: &[usize]) -> Option<f64> {
None
}
}
pub fn r_hat_f64(chains: &[Vec<Trace>], addr: &Address) -> f64 {
let chain_values: Vec<Vec<f64>> = chains
.iter()
.map(|chain| extract_f64_values(chain, addr))
.collect();
split_r_hat_from_f64_chains(&chain_values)
}
pub fn classic_r_hat_f64(chains: &[Vec<Trace>], addr: &Address) -> f64 {
let chain_values: Vec<Vec<f64>> = chains
.iter()
.map(|chain| extract_f64_values(chain, addr))
.collect();
r_hat_from_f64_chains(&chain_values)
}
fn split_f64_chains(chain_values: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut out = Vec::with_capacity(chain_values.len() * 2);
for c in chain_values {
let half = c.len() / 2;
if half == 0 {
out.push(c.clone());
continue;
}
out.push(c[..half].to_vec());
out.push(c[half..2 * half].to_vec());
}
out
}
fn split_r_hat_from_f64_chains(chain_values: &[Vec<f64>]) -> f64 {
let split = split_f64_chains(chain_values);
r_hat_from_f64_chains(&split)
}
fn r_hat_from_f64_chains(chain_values: &[Vec<f64>]) -> f64 {
if chain_values.len() < 2 {
return 1.0; }
if chain_values.iter().any(|v| v.is_empty()) {
return f64::NAN; }
let m = chain_values.len() as f64; let n = chain_values[0].len() as f64;
let chain_means: Vec<f64> = chain_values
.iter()
.map(|values| values.iter().sum::<f64>() / values.len() as f64)
.collect();
let overall_mean = chain_means.iter().sum::<f64>() / m;
let b = n / (m - 1.0)
* chain_means
.iter()
.map(|mean| (mean - overall_mean).powi(2))
.sum::<f64>();
let within_variances: Vec<f64> = chain_values
.iter()
.zip(&chain_means)
.map(|(values, &mean)| values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1.0))
.collect();
let w = within_variances.iter().sum::<f64>() / m;
let var_plus = ((n - 1.0) / n) * w + (1.0 / n) * b;
(var_plus / w).sqrt()
}
pub fn effective_sample_size(values: &[f64]) -> f64 {
effective_sample_size_mcmc(values)
}
#[derive(Debug, Clone)]
pub struct ParameterSummary {
pub mean: f64,
pub std: f64,
pub quantiles: HashMap<String, f64>, pub r_hat: f64,
pub ess: f64,
}
pub fn summarize_f64_parameter(chains: &[Vec<Trace>], addr: &Address) -> ParameterSummary {
let all_values: Vec<f64> = chains
.iter()
.flat_map(|chain| extract_f64_values(chain, addr))
.collect();
if all_values.is_empty() {
return ParameterSummary {
mean: f64::NAN,
std: f64::NAN,
quantiles: HashMap::new(),
r_hat: f64::NAN,
ess: 0.0,
};
}
let mean = all_values.iter().sum::<f64>() / all_values.len() as f64;
let variance =
all_values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (all_values.len() - 1) as f64;
let std = variance.sqrt();
let mut sorted_values = all_values.clone();
sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut quantiles = HashMap::new();
let percentiles = [
("2.5%", 0.025),
("25%", 0.25),
("50%", 0.5),
("75%", 0.75),
("97.5%", 0.975),
];
for (name, p) in percentiles {
let idx = ((sorted_values.len() - 1) as f64 * p).round() as usize;
quantiles.insert(name.to_string(), sorted_values[idx]);
}
let r_hat_val = r_hat_f64(chains, addr);
let per_chain_values: Vec<Vec<f64>> = chains
.iter()
.map(|chain| extract_f64_values(chain, addr))
.collect();
let ess_val = effective_sample_size_multichain(&per_chain_values);
ParameterSummary {
mean,
std,
quantiles,
r_hat: r_hat_val,
ess: ess_val,
}
}
pub fn print_diagnostics(chains: &[Vec<Trace>]) {
if chains.is_empty() || chains[0].is_empty() {
println!("No chains or empty chains provided");
return;
}
let mut all_addresses = std::collections::HashSet::new();
for chain in chains {
for trace in chain {
for addr in trace.choices.keys() {
all_addresses.insert(addr.clone());
}
}
}
println!("MCMC Diagnostics:");
println!(
"{:<15} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}",
"Parameter", "Mean", "Std", "2.5%", "50%", "97.5%", "R-hat", "ESS"
);
println!("{}", "-".repeat(80));
for addr in &all_addresses {
let summary = summarize_f64_parameter(chains, addr);
println!(
"{:<15} {:>8.3} {:>8.3} {:>8.3} {:>8.3} {:>8.3} {:>8.3} {:>8.0}",
addr.to_string(),
summary.mean,
summary.std,
summary.quantiles.get("2.5%").unwrap_or(&f64::NAN),
summary.quantiles.get("50%").unwrap_or(&f64::NAN),
summary.quantiles.get("97.5%").unwrap_or(&f64::NAN),
summary.r_hat,
summary.ess
);
}
let all_r_hats: Vec<f64> = all_addresses
.iter()
.map(|addr| summarize_f64_parameter(chains, addr).r_hat)
.filter(|&x| x.is_finite())
.collect();
if !all_r_hats.is_empty() {
let max_r_hat = all_r_hats.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let avg_r_hat = all_r_hats.iter().sum::<f64>() / all_r_hats.len() as f64;
println!("\nConvergence Assessment:");
if max_r_hat < 1.01 {
println!("✓ Excellent convergence (max R-hat = {:.3})", max_r_hat);
} else if max_r_hat < 1.1 {
println!("âš Good convergence (max R-hat = {:.3})", max_r_hat);
} else {
println!(
"✗ Poor convergence (max R-hat = {:.3}) - consider more samples",
max_r_hat
);
}
println!(" Average R-hat: {:.3}", avg_r_hat);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::addr;
use crate::core::distribution::*;
use crate::core::model::{observe, sample, ModelExt};
use crate::runtime::handler::run;
use crate::runtime::interpreters::PriorHandler;
use rand::rngs::StdRng;
use rand::SeedableRng;
fn generate_chain(seed: u64, n: usize) -> Vec<Trace> {
let mut rng = StdRng::seed_from_u64(seed);
let mut traces = Vec::new();
for _ in 0..n {
let (_a, t) = run(
PriorHandler {
rng: &mut rng,
trace: Trace::default(),
},
sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap())
.and_then(|mu| observe(addr!("y"), Normal::new(mu, 0.5).unwrap(), 0.0)),
);
traces.push(t);
}
traces
}
#[test]
fn extractors_return_expected_values() {
let chain = generate_chain(1, 5);
let vals = extract_f64_values(&chain, &addr!("mu"));
assert_eq!(vals.len(), 5);
let bools = extract_bool_values(&chain, &addr!("mu"));
assert!(bools.is_empty());
}
#[test]
fn r_hat_and_summary_compute() {
let chains = vec![generate_chain(2, 10), generate_chain(3, 10)];
let r = r_hat_f64(&chains, &addr!("mu"));
assert!(r.is_finite());
let summary = summarize_f64_parameter(&chains, &addr!("mu"));
assert!(summary.mean.is_finite());
assert!(summary.std.is_finite());
}
#[test]
fn diagnostics_trait_for_other_types() {
let chains = vec![generate_chain(4, 5), generate_chain(5, 5)];
let r_u64 = <u64 as Diagnostics<u64>>::r_hat(&chains, &addr!("mu"));
let _ = r_u64;
let r_usize = <usize as Diagnostics<usize>>::r_hat(&chains, &addr!("mu"));
assert!(r_usize.is_none());
}
#[test]
fn print_diagnostics_with_multiple_addresses() {
let mut rng = StdRng::seed_from_u64(6);
let mut chain = Vec::new();
for _ in 0..5 {
let (_a, mut t) = run(
PriorHandler {
rng: &mut rng,
trace: Trace::default(),
},
sample(addr!("a1"), Normal::new(0.0, 1.0).unwrap())
.and_then(|x| observe(addr!("obs"), Normal::new(x, 1.0).unwrap(), 0.0)),
);
t.insert_choice(
addr!("a2"),
crate::runtime::trace::ChoiceValue::F64(1.0),
-0.5,
);
chain.push(t);
}
let chains = vec![chain.clone(), chain];
print_diagnostics(&chains);
}
}