use std::num::NonZeroUsize;
use owalnuts::walnutpie::{
DiagonalMass, KernelTuning, MultiChainOutput, RunConfig, Target, TargetError,
TargetEvaluationAdmissionLimit, TargetEvaluationBudget, WarmupConfig,
sample_chains_with_target_budget,
};
const FUNNEL_DIMENSION: usize = 10;
struct Funnel;
impl Target for Funnel {
fn dimension(&self) -> usize {
FUNNEL_DIMENSION
}
fn log_density_gradient(
&self,
position: &[f64],
gradient: &mut [f64],
) -> Result<f64, TargetError> {
let omega = position[0];
let inverse_variance = (-omega).exp();
if !inverse_variance.is_finite() {
return Err(TargetError::recoverable("exp(-omega) overflowed"));
}
let sum_squares: f64 = position[1..].iter().map(|x| x * x).sum();
let tail = (FUNNEL_DIMENSION - 1) as f64;
gradient[0] = -omega / 9.0 - 0.5 * tail + 0.5 * inverse_variance * sum_squares;
for (g, x) in gradient[1..].iter_mut().zip(&position[1..]) {
*g = -inverse_variance * x;
}
Ok(-omega * omega / 18.0 - 0.5 * tail * omega - 0.5 * inverse_variance * sum_squares)
}
}
const LOG_2PI: f64 = 1.837_877_066_409_345_3;
const SCHOOL_Y: [f64; 8] = [28., 8., -3., 7., -1., 1., 18., 12.];
const SCHOOL_SE: [f64; 8] = [15., 10., 16., 11., 9., 11., 10., 18.];
fn normal_log_density(x: f64, mean: f64, sd: f64) -> f64 {
-0.5 * LOG_2PI - sd.ln() - 0.5 * ((x - mean) / sd).powi(2)
}
struct EightSchools;
impl Target for EightSchools {
fn dimension(&self) -> usize {
10
}
fn log_density_gradient(&self, q: &[f64], gradient: &mut [f64]) -> Result<f64, TargetError> {
let mu = q[0];
let log_tau = q[1];
let tau = log_tau.exp();
let z = &q[2..];
let mut value = normal_log_density(mu, 0., 5.)
+ (2. / (std::f64::consts::PI * 5. * (1. + (tau / 5.).powi(2)))).ln()
+ log_tau;
gradient.fill(0.);
gradient[0] = -mu / 25.;
gradient[1] = 1. - 2. * tau * tau / (25. + tau * tau);
for j in 0..8 {
let theta = mu + tau * z[j];
let residual = SCHOOL_Y[j] - theta;
let likelihood_gradient = residual / SCHOOL_SE[j].powi(2);
value += normal_log_density(SCHOOL_Y[j], theta, SCHOOL_SE[j])
+ normal_log_density(z[j], 0., 1.);
gradient[0] += likelihood_gradient;
gradient[1] += likelihood_gradient * tau * z[j];
gradient[j + 2] = -z[j] + likelihood_gradient * tau;
}
if value.is_finite() && gradient.iter().all(|x| x.is_finite()) {
Ok(value)
} else {
Err(TargetError::new("nonfinite target evaluation"))
}
}
}
fn nz(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).expect("nonzero")
}
fn fnv(hash: &mut u64, value: u64) {
*hash ^= value;
*hash = hash.wrapping_mul(0x100_0000_01b3);
}
fn fingerprint(output: &MultiChainOutput) -> (u64, usize) {
let mut hash = 0xcbf2_9ce4_8422_2325;
let mut calls = 0;
for chain in output.chains() {
for value in chain.samples() {
fnv(&mut hash, value.to_bits());
}
let chain_calls = chain.telemetry().total().target_calls_total();
fnv(&mut hash, chain_calls as u64);
calls += chain_calls;
}
(hash, calls)
}
#[test]
fn funnel_paper_tuning_four_chains_are_bit_exact() {
let tuning = KernelTuning::new(0.36, nz(10), nz(1), nz(8), 0.21).unwrap();
let config = RunConfig::new(0, nz(400), 0x5eed_f0f0).with_tuning(tuning);
let starts: Vec<Vec<f64>> = [-3.0, -1.0, 1.0, 3.0]
.into_iter()
.map(|omega| {
let mut q = vec![0.0; FUNNEL_DIMENSION];
q[0] = omega;
q
})
.collect();
let worst = config.worst_case_target_evaluations(nz(4)).unwrap();
let output = sample_chains_with_target_budget(
&Funnel,
&starts,
&DiagonalMass::identity(nz(FUNNEL_DIMENSION)),
&config,
nz(1),
TargetEvaluationAdmissionLimit::new(nz(worst)),
&TargetEvaluationBudget::new(nz(worst)),
)
.unwrap();
let (hash, calls) = fingerprint(&output);
check("funnel", hash, calls, FUNNEL_FINGERPRINT, FUNNEL_CALLS);
}
#[test]
fn eight_schools_adapted_four_chains_are_bit_exact() {
let tuning = KernelTuning::new(0.3, nz(8), nz(1), nz(8), 1.0)
.unwrap()
.with_divergence_threshold(1000.0)
.unwrap();
let warmup = WarmupConfig::new(0.95).unwrap().with_mass_adaptation(true);
let config = RunConfig::new(300, nz(300), 0x5eed_0008)
.with_tuning(tuning)
.with_warmup(warmup);
let starts: Vec<Vec<f64>> = [-2., -1., 0., 1.]
.into_iter()
.map(|log_tau| {
let mut q = vec![0.0; 10];
q[1] = log_tau;
q
})
.collect();
let worst = config.worst_case_target_evaluations(nz(4)).unwrap();
let output = sample_chains_with_target_budget(
&EightSchools,
&starts,
&DiagonalMass::identity(nz(10)),
&config,
nz(1),
TargetEvaluationAdmissionLimit::new(nz(worst)),
&TargetEvaluationBudget::new(nz(worst)),
)
.unwrap();
let (hash, calls) = fingerprint(&output);
check(
"eight schools",
hash,
calls,
EIGHT_SCHOOLS_FINGERPRINT,
EIGHT_SCHOOLS_CALLS,
);
}
#[cfg(windows)]
const FUNNEL_FINGERPRINT: Option<u64> = Some(0x387f_e4f4_c00c_3a05);
#[cfg(target_os = "linux")]
const FUNNEL_FINGERPRINT: Option<u64> = Some(0x9da8_4e9d_a471_5afb);
#[cfg(not(any(windows, target_os = "linux")))]
const FUNNEL_FINGERPRINT: Option<u64> = None;
const FUNNEL_CALLS: usize = 74_014;
#[cfg(windows)]
const EIGHT_SCHOOLS_FINGERPRINT: Option<u64> = if cfg!(debug_assertions) {
Some(0xcd59_b77f_fe72_c8b6)
} else {
Some(0x5600_757f_2a08_6a12)
};
#[cfg(target_os = "linux")]
const EIGHT_SCHOOLS_FINGERPRINT: Option<u64> = if cfg!(debug_assertions) {
Some(0xf0e5_5a36_305c_4077)
} else {
None
};
#[cfg(not(any(windows, target_os = "linux")))]
const EIGHT_SCHOOLS_FINGERPRINT: Option<u64> = None;
const EIGHT_SCHOOLS_CALLS: usize = 38_464;
fn check(label: &str, hash: u64, calls: usize, expected: Option<u64>, expected_calls: usize) {
assert_eq!(calls, expected_calls, "{label} made {calls} target calls");
match expected {
Some(expected) => assert_eq!(
hash, expected,
"{label} fingerprint {hash:016x} with {calls} calls"
),
None => eprintln!(
"{label}: unpinned platform/profile, fingerprint {hash:016x} with {calls} calls"
),
}
}