#![allow(dead_code)]
use owalnuts::walnutpie::Target;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rand_distr::StandardNormal;
#[derive(Clone)]
pub struct Z {
pub q: Vec<f64>,
pub p: Vec<f64>,
pub g: Vec<f64>,
pub lp: f64,
}
struct Subtree {
valid: bool,
rho: Vec<f64>,
p_beg: Vec<f64>,
p_end: Vec<f64>,
propose: Z,
log_weight: f64,
states: usize,
selected_offset: usize,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct RefStats {
pub depth: usize,
pub leapfrogs: usize,
pub divergent: bool,
pub max_depth: bool,
pub orbit_states: usize,
pub selected_index: usize,
pub initial_index: usize,
}
pub struct RefNuts<'a, T: Target> {
target: &'a T,
pub step: f64,
pub inv_mass: Vec<f64>,
pub max_depth: usize,
pub max_delta_h: f64,
leapfrogs: usize,
divergent: bool,
sum_metro_prob: f64,
}
impl<'a, T: Target> RefNuts<'a, T> {
pub fn new(target: &'a T, step: f64, inv_mass: Vec<f64>, max_depth: usize) -> Self {
Self {
target,
step,
inv_mass,
max_depth,
max_delta_h: 1000.0,
leapfrogs: 0,
divergent: false,
sum_metro_prob: 0.0,
}
}
pub fn hamiltonian(&self, z: &Z) -> f64 {
let kinetic: f64 =
z.p.iter()
.zip(&self.inv_mass)
.map(|(p, m)| 0.5 * p * p * m)
.sum();
-z.lp + kinetic
}
pub fn evaluate(&self, z: &mut Z) {
match self.target.log_density_gradient(&z.q, &mut z.g) {
Ok(lp) if lp.is_finite() && z.g.iter().all(|g| g.is_finite()) => z.lp = lp,
_ => {
z.lp = f64::NEG_INFINITY;
z.g.fill(0.0);
}
}
}
pub fn initial(&self, q: Vec<f64>) -> Z {
let d = q.len();
let mut z = Z {
q,
p: vec![0.0; d],
g: vec![0.0; d],
lp: 0.0,
};
self.evaluate(&mut z);
z
}
fn leapfrog(&mut self, z: &mut Z, eps: f64) {
for (p, g) in z.p.iter_mut().zip(&z.g) {
*p += 0.5 * eps * g;
}
for ((q, p), m) in z.q.iter_mut().zip(&z.p).zip(&self.inv_mass) {
*q += eps * m * p;
}
self.evaluate(z);
for (p, g) in z.p.iter_mut().zip(&z.g) {
*p += 0.5 * eps * g;
}
self.leapfrogs += 1;
}
fn criterion(&self, p_minus: &[f64], p_plus: &[f64], rho: &[f64]) -> bool {
let dot = |p: &[f64]| -> f64 {
p.iter()
.zip(rho)
.zip(&self.inv_mass)
.map(|((p, r), m)| p * m * r)
.sum()
};
dot(p_plus) > 0.0 && dot(p_minus) > 0.0
}
fn build_tree(
&mut self,
depth: usize,
z: &mut Z,
sign: f64,
h0: f64,
rng: &mut SmallRng,
) -> Subtree {
if depth == 0 {
self.leapfrog(z, sign * self.step);
let mut h = self.hamiltonian(z);
if h.is_nan() {
h = f64::INFINITY;
}
if h - h0 > self.max_delta_h {
self.divergent = true;
}
self.sum_metro_prob += (h0 - h).min(0.0).exp();
return Subtree {
valid: !self.divergent,
rho: z.p.clone(),
p_beg: z.p.clone(),
p_end: z.p.clone(),
propose: z.clone(),
log_weight: h0 - h,
states: 1,
selected_offset: 0,
};
}
let init = self.build_tree(depth - 1, z, sign, h0, rng);
if !init.valid {
return init;
}
let fin = self.build_tree(depth - 1, z, sign, h0, rng);
if !fin.valid {
return fin;
}
let log_weight = log_sum_exp(init.log_weight, fin.log_weight);
let take_fin = rng.random::<f64>().ln() < fin.log_weight - log_weight;
let selected_offset = match (sign > 0.0, take_fin) {
(true, true) => init.states + fin.selected_offset,
(true, false) => init.selected_offset,
(false, true) => fin.selected_offset,
(false, false) => fin.states + init.selected_offset,
};
let propose = if take_fin { fin.propose } else { init.propose };
let rho: Vec<f64> = init.rho.iter().zip(&fin.rho).map(|(a, b)| a + b).collect();
let mut persist = self.criterion(&init.p_beg, &fin.p_end, &rho);
let ext: Vec<f64> = init
.rho
.iter()
.zip(&fin.p_beg)
.map(|(a, b)| a + b)
.collect();
persist &= self.criterion(&init.p_beg, &fin.p_beg, &ext);
let ext: Vec<f64> = fin
.rho
.iter()
.zip(&init.p_end)
.map(|(a, b)| a + b)
.collect();
persist &= self.criterion(&init.p_end, &fin.p_end, &ext);
Subtree {
valid: persist,
rho,
p_beg: init.p_beg,
p_end: fin.p_end,
propose,
log_weight,
states: init.states + fin.states,
selected_offset,
}
}
pub fn transition(&mut self, current: &Z, rng: &mut SmallRng) -> (Z, RefStats) {
let mut z = current.clone();
for (p, m) in z.p.iter_mut().zip(&self.inv_mass) {
*p = rng.sample::<f64, _>(StandardNormal) / m.sqrt();
}
self.leapfrogs = 0;
self.divergent = false;
self.sum_metro_prob = 0.0;
let h0 = self.hamiltonian(&z);
let mut z_fwd = z.clone();
let mut z_bck = z.clone();
let mut sample = z.clone();
let mut rho = z.p.clone();
let (mut p_bck, mut p_fwd) = (z.p.clone(), z.p.clone());
let mut log_weight = 0.0;
let mut depth = 0;
let mut hit_max = true;
let mut orbit_states = 1usize;
let mut selected_index = 0usize;
let mut initial_index = 0usize;
while depth < self.max_depth {
let forward = rng.random::<f64>() > 0.5;
let sub = if forward {
self.build_tree(depth, &mut z_fwd, 1.0, h0, rng)
} else {
self.build_tree(depth, &mut z_bck, -1.0, h0, rng)
};
if !sub.valid {
hit_max = false;
break;
}
depth += 1;
let take_new = sub.log_weight > log_weight
|| rng.random::<f64>().ln() < sub.log_weight - log_weight;
if forward {
if take_new {
selected_index = orbit_states + sub.selected_offset;
}
} else {
initial_index += sub.states;
selected_index = if take_new {
sub.selected_offset
} else {
selected_index + sub.states
};
}
orbit_states += sub.states;
if take_new {
sample = sub.propose;
}
log_weight = log_sum_exp(log_weight, sub.log_weight);
let merged: Vec<f64> = rho.iter().zip(&sub.rho).map(|(a, b)| a + b).collect();
let (tree_near, tree_far) = if forward {
(&p_fwd, &p_bck)
} else {
(&p_bck, &p_fwd)
};
let mut persist = self.criterion(tree_far, &sub.p_end, &merged);
let ext: Vec<f64> = rho.iter().zip(&sub.p_beg).map(|(a, b)| a + b).collect();
persist &= self.criterion(tree_far, &sub.p_beg, &ext);
let ext: Vec<f64> = sub.rho.iter().zip(tree_near).map(|(a, b)| a + b).collect();
persist &= self.criterion(tree_near, &sub.p_end, &ext);
rho = merged;
if forward {
p_fwd = sub.p_end;
} else {
p_bck = sub.p_end;
}
if !persist {
hit_max = false;
break;
}
}
let stats = RefStats {
depth,
leapfrogs: self.leapfrogs,
divergent: self.divergent,
max_depth: hit_max,
orbit_states,
selected_index,
initial_index,
};
(sample, stats)
}
}
pub fn log_sum_exp(a: f64, b: f64) -> f64 {
if a == f64::NEG_INFINITY {
return b;
}
if b == f64::NEG_INFINITY {
return a;
}
let m = a.max(b);
m + ((a - m).exp() + (b - m).exp()).ln()
}
pub fn run_chain<T: Target>(
target: &T,
step: f64,
inv_mass: Vec<f64>,
start: &[f64],
draws: usize,
max_depth: usize,
seed: u64,
) -> (Vec<f64>, Vec<RefStats>) {
let mut nuts = RefNuts::new(target, step, inv_mass, max_depth);
let mut rng = SmallRng::seed_from_u64(seed);
let mut z = nuts.initial(start.to_vec());
let d = start.len();
let mut samples = Vec::with_capacity(draws * d);
let mut stats = Vec::with_capacity(draws);
for _ in 0..draws {
let (next, s) = nuts.transition(&z, &mut rng);
z = next;
samples.extend_from_slice(&z.q);
stats.push(s);
}
(samples, stats)
}