use hammer_and_sample::{sample, Model, Parallel};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg64;
use std::sync::Arc;
type LogLikelihood<'b,const D: usize, const P: usize> = dyn Fn(&[f64; D], &[f64; P]) -> f64 + Send + Sync + 'b;
struct Data<'a, 'b, const D: usize, const P: usize> {
data: &'a [[f64; D]],
loglikelihood: Arc<LogLikelihood<'b,D,P>>,
}
impl<const D: usize, const K: usize> Model for Data<'_, '_, D, K> {
type Params = [f64; K];
fn log_prob(&self, params: &Self::Params) -> f64 {
self.data
.iter()
.map(|d| (self.loglikelihood)(d, params))
.sum()
}
}
pub fn parameter_inference_uniform_prior<
'a,
'b,
L,
const D: usize,
const P: usize,
const W: usize,
const B: usize,
const S: usize,
>(
data: &'a [[f64; D]],
bounds: &[[f64; 2]; P],
loglikelihood: &L,
) -> ([f64; P], Vec<[f64; P]>)
where
L: Fn(&[f64; D], &[f64; P]) -> f64 + Send + Sync + 'b,
{
bounds.iter().enumerate().for_each(|(i, [low, high])| {
assert!(
high > low,
"Invalid bounds for parameter {i} is invalid. low = {low}, high = {high}"
)
});
let data = Data {
data,
loglikelihood: Arc::new(loglikelihood),
};
let walkers = (0..W as u64).map(|seed| {
let mut rng = Pcg64::seed_from_u64(seed);
let p = (0..P)
.map(|i| rng.gen_range(bounds[i][0]..=bounds[i][1]))
.collect::<Vec<_>>()
.try_into()
.expect("shape should be as expected");
(p, rng)
});
let (mut chain, _accepted) = sample(&data, walkers, S, &Parallel);
println!("chain[..15] = {:?}", &chain[..15]);
println!(
"chain[-15..] = {:?}",
&chain.iter().rev().take(15).rev().collect::<Vec<_>>()
);
let chain: Vec<[f64; P]> = chain.drain(W * B..).collect();
let norm = chain.len() as f64;
(chain
.iter()
.fold([0.0; P], |mut acc, p| {
acc.iter_mut().enumerate().for_each(|(i, a)| *a += p[i]);
acc
})
.map(|p| p / norm), chain)
}
#[test]
fn test_coin_flip() {
const P: usize = 1;
const D: usize = 1;
let loglikelihood = |x: &[f64; D], p: &[f64; P]| {
if x[0] == 1.0 {
p[0].ln()
} else if x[0] == 0.0 {
(1.0 - p[0]).ln()
} else {
unreachable!()
}
};
let data = [vec![[1.0]; 1000], vec![[0.0]; 500]].concat();
let bounds = [[0.0, 1.0]];
const WALKERS: usize = 32;
const BURN_IN: usize = 100;
const SAMPLES: usize = 1000;
let (params, _chain) = parameter_inference_uniform_prior::<_, D, P, WALKERS, BURN_IN, SAMPLES>(
&data,
&bounds,
&loglikelihood,
);
assert!(params[0] > 0.65 && params[0] < 0.67);
}
#[test]
fn test_gaussian() {
use rand_distr::{Distribution, Normal};
use std::f64::consts::PI;
const P: usize = 2;
const D: usize = 1;
let loglikelihood = |x: &[f64; 1], p: &[f64; P]| {
if p[1] < 0.0 {
return std::f64::NEG_INFINITY;
} else {
-0.5 * (((x[0] - p[0]) / p[1]).powf(2.0) + (2.0 * PI * p[1].powi(2)).ln())
}
};
let actual_mean: f64 = 1.0;
let actual_std: f64 = 0.3;
let normal = Normal::new(actual_mean, actual_std).unwrap();
let mut thread_rng = rand::thread_rng();
let data: [[f64; 1]; 10_000] = [(); 10_000].map(|_| [normal.sample(&mut thread_rng)]);
println!("data[..15] = {:?}", &data[..15]);
let bounds = [[-8.0, 12.0], [1e-2, 1e1]];
const WALKERS: usize = 64;
const BURN_IN: usize = 100;
const SAMPLES: usize = 1000;
let (params, _chain) = parameter_inference_uniform_prior::<_, D, P, WALKERS, BURN_IN, SAMPLES>(
&data,
&bounds,
&loglikelihood,
);
println!("params = {params:?}");
assert!(params[0] > 0.9 && params[0] < 1.1 && params[1] > 0.29 && params[1] < 0.31);
}
#[test]
#[should_panic]
fn test_invalid_bound() {
const P: usize = 1;
const D: usize = 1;
let loglikelihood = |x: &[f64; 1], p: &[f64; P]| {
if x[0] == 1.0 {
p[0].ln()
} else if x[0] == 0.0 {
(1.0 - p[0]).ln()
} else {
unreachable!("test where we only construct float arrays with 1.0 and 0.0")
}
};
let data = [vec![[1.0]; 1000], vec![[0.0]; 500]].concat();
let bounds = [[1.0, 0.0]];
const WALKERS: usize = 32;
const BURN_IN: usize = 100;
const SAMPLES: usize = 1000;
let (params, _chain) = parameter_inference_uniform_prior::<_, D, P, WALKERS, BURN_IN, SAMPLES>(
&data,
&bounds,
&loglikelihood,
);
assert!(params[0] > 0.65 && params[0] < 0.67);
}