use crate::array::Array;
use crate::new_modules::probabilistic::{validate_positive, ProbabilisticError, Result};
use scirs2_core::ndarray::Array1;
use scirs2_core::random::{thread_rng, Rng, RngExt};
use std::collections::HashMap;
pub trait ProposalDistribution {
fn propose<R: Rng>(&self, current: &[f64], rng: &mut R) -> Result<Vec<f64>>;
fn log_prob(&self, from: &[f64], to: &[f64]) -> f64;
}
#[derive(Debug, Clone)]
pub struct GaussianProposal {
step_size: f64,
}
impl GaussianProposal {
pub fn new(step_size: f64) -> Result<Self> {
validate_positive(step_size, "step_size")?;
Ok(Self { step_size })
}
}
impl ProposalDistribution for GaussianProposal {
fn propose<R: Rng>(&self, current: &[f64], rng: &mut R) -> Result<Vec<f64>> {
let mut proposal = current.to_vec();
for x in &mut proposal {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
*x += self.step_size * z;
}
Ok(proposal)
}
fn log_prob(&self, _from: &[f64], _to: &[f64]) -> f64 {
0.0
}
}
pub struct MetropolisHastings<F, P>
where
F: Fn(&[f64]) -> f64,
P: ProposalDistribution,
{
log_posterior: F,
proposal: P,
}
impl<F, P> MetropolisHastings<F, P>
where
F: Fn(&[f64]) -> f64,
P: ProposalDistribution,
{
pub fn new(log_posterior: F, proposal: P) -> Self {
Self {
log_posterior,
proposal,
}
}
pub fn sample<R: Rng>(
&self,
initial_state: &[f64],
n_iterations: usize,
burn_in: usize,
rng: &mut R,
) -> Result<MCMCResult> {
if burn_in >= n_iterations {
return Err(ProbabilisticError::InvalidParameter {
parameter: "burn_in".to_string(),
message: "burn_in must be less than n_iterations".to_string(),
});
}
let dim = initial_state.len();
let n_samples = n_iterations - burn_in;
let mut samples = Vec::with_capacity(n_samples);
let mut current_state = initial_state.to_vec();
let mut current_log_prob = (self.log_posterior)(¤t_state);
if !current_log_prob.is_finite() {
return Err(ProbabilisticError::SamplingError {
sampler: "Metropolis-Hastings".to_string(),
iteration: 0,
message: "Initial log probability is not finite".to_string(),
});
}
let mut n_accepted = 0;
for iter in 0..n_iterations {
let proposed_state = self.proposal.propose(¤t_state, rng)?;
let proposed_log_prob = (self.log_posterior)(&proposed_state);
if !proposed_log_prob.is_finite() {
if iter >= burn_in {
samples.push(current_state.clone());
}
continue;
}
let log_proposal_ratio = self.proposal.log_prob(&proposed_state, ¤t_state)
- self.proposal.log_prob(¤t_state, &proposed_state);
let log_alpha = proposed_log_prob - current_log_prob + log_proposal_ratio;
let u: f64 = rng.random();
if u.ln() < log_alpha {
current_state = proposed_state;
current_log_prob = proposed_log_prob;
n_accepted += 1;
}
if iter >= burn_in {
samples.push(current_state.clone());
}
}
let acceptance_rate = n_accepted as f64 / n_iterations as f64;
Ok(MCMCResult {
samples,
acceptance_rate,
n_iterations,
burn_in,
})
}
}
#[derive(Debug, Clone)]
pub struct MCMCResult {
pub samples: Vec<Vec<f64>>,
pub acceptance_rate: f64,
pub n_iterations: usize,
pub burn_in: usize,
}
impl MCMCResult {
pub fn mean(&self) -> Vec<f64> {
if self.samples.is_empty() {
return vec![];
}
let dim = self.samples[0].len();
let n = self.samples.len() as f64;
let mut mean = vec![0.0; dim];
for sample in &self.samples {
for (i, &x) in sample.iter().enumerate() {
mean[i] += x / n;
}
}
mean
}
pub fn std(&self) -> Vec<f64> {
if self.samples.is_empty() {
return vec![];
}
let mean = self.mean();
let dim = self.samples[0].len();
let n = self.samples.len() as f64;
let mut variance = vec![0.0; dim];
for sample in &self.samples {
for (i, &x) in sample.iter().enumerate() {
let diff = x - mean[i];
variance[i] += diff * diff / n;
}
}
variance.iter().map(|v| v.sqrt()).collect()
}
pub fn effective_sample_size(&self) -> Vec<f64> {
if self.samples.len() < 10 {
return vec![self.samples.len() as f64; self.samples[0].len()];
}
let dim = self.samples[0].len();
let mut ess = Vec::with_capacity(dim);
for d in 0..dim {
let values: Vec<f64> = self.samples.iter().map(|s| s[d]).collect();
ess.push(compute_ess(&values));
}
ess
}
}
fn compute_ess(values: &[f64]) -> f64 {
let n = values.len();
if n < 10 {
return n as f64;
}
let mean = values.iter().sum::<f64>() / n as f64;
let variance = values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
if variance < 1e-12 {
return n as f64; }
let max_lag = n / 2;
let mut sum_rho = 0.0;
for lag in 1..max_lag {
let mut cov = 0.0;
for i in 0..(n - lag) {
cov += (values[i] - mean) * (values[i + lag] - mean);
}
cov /= (n - lag) as f64;
let rho = cov / variance;
if rho < 0.0 {
break;
}
sum_rho += rho;
}
n as f64 / (1.0 + 2.0 * sum_rho).max(1.0)
}
pub trait ConditionalSampler {
fn sample_conditional<R: Rng>(
&self,
index: usize,
current_state: &[f64],
rng: &mut R,
) -> Result<f64>;
}
pub struct GibbsSampler<S>
where
S: ConditionalSampler,
{
conditional_sampler: S,
dim: usize,
}
impl<S> GibbsSampler<S>
where
S: ConditionalSampler,
{
pub fn new(conditional_sampler: S, dim: usize) -> Self {
Self {
conditional_sampler,
dim,
}
}
pub fn sample<R: Rng>(
&self,
initial_state: &[f64],
n_iterations: usize,
burn_in: usize,
rng: &mut R,
) -> Result<MCMCResult> {
if initial_state.len() != self.dim {
return Err(ProbabilisticError::DimensionMismatch {
expected: vec![self.dim],
actual: vec![initial_state.len()],
operation: "Gibbs sampling initialization".to_string(),
});
}
if burn_in >= n_iterations {
return Err(ProbabilisticError::InvalidParameter {
parameter: "burn_in".to_string(),
message: "burn_in must be less than n_iterations".to_string(),
});
}
let n_samples = n_iterations - burn_in;
let mut samples = Vec::with_capacity(n_samples);
let mut current_state = initial_state.to_vec();
for iter in 0..n_iterations {
for i in 0..self.dim {
current_state[i] =
self.conditional_sampler
.sample_conditional(i, ¤t_state, rng)?;
}
if iter >= burn_in {
samples.push(current_state.clone());
}
}
Ok(MCMCResult {
samples,
acceptance_rate: 1.0, n_iterations,
burn_in,
})
}
}
pub struct HamiltonianMC<F, G>
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> Vec<f64>,
{
log_posterior: F,
grad_log_posterior: G,
step_size: f64,
n_steps: usize,
}
impl<F, G> HamiltonianMC<F, G>
where
F: Fn(&[f64]) -> f64,
G: Fn(&[f64]) -> Vec<f64>,
{
pub fn new(
log_posterior: F,
grad_log_posterior: G,
step_size: f64,
n_steps: usize,
) -> Result<Self> {
validate_positive(step_size, "step_size")?;
if n_steps == 0 {
return Err(ProbabilisticError::InvalidParameter {
parameter: "n_steps".to_string(),
message: "n_steps must be positive".to_string(),
});
}
Ok(Self {
log_posterior,
grad_log_posterior,
step_size,
n_steps,
})
}
fn leapfrog(&self, q: &[f64], p: &[f64]) -> Result<(Vec<f64>, Vec<f64>)> {
let mut q_new = q.to_vec();
let mut p_new = p.to_vec();
let grad = (self.grad_log_posterior)(&q_new);
for i in 0..p_new.len() {
p_new[i] += 0.5 * self.step_size * grad[i];
}
for _ in 0..(self.n_steps - 1) {
for i in 0..q_new.len() {
q_new[i] += self.step_size * p_new[i];
}
let grad = (self.grad_log_posterior)(&q_new);
for i in 0..p_new.len() {
p_new[i] += self.step_size * grad[i];
}
}
for i in 0..q_new.len() {
q_new[i] += self.step_size * p_new[i];
}
let grad = (self.grad_log_posterior)(&q_new);
for i in 0..p_new.len() {
p_new[i] += 0.5 * self.step_size * grad[i];
}
Ok((q_new, p_new))
}
fn hamiltonian(&self, q: &[f64], p: &[f64]) -> f64 {
let kinetic = 0.5 * p.iter().map(|&pi| pi * pi).sum::<f64>();
let potential = -(self.log_posterior)(q);
kinetic + potential
}
pub fn sample<R: Rng>(
&self,
initial_state: &[f64],
n_iterations: usize,
burn_in: usize,
rng: &mut R,
) -> Result<MCMCResult> {
if burn_in >= n_iterations {
return Err(ProbabilisticError::InvalidParameter {
parameter: "burn_in".to_string(),
message: "burn_in must be less than n_iterations".to_string(),
});
}
let dim = initial_state.len();
let n_samples = n_iterations - burn_in;
let mut samples = Vec::with_capacity(n_samples);
let mut current_state = initial_state.to_vec();
let mut n_accepted = 0;
for iter in 0..n_iterations {
let mut momentum = vec![0.0; dim];
for p in &mut momentum {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
*p = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
}
let current_h = self.hamiltonian(¤t_state, &momentum);
let (proposed_state, proposed_momentum) = self.leapfrog(¤t_state, &momentum)?;
let proposed_h = self.hamiltonian(&proposed_state, &proposed_momentum);
let log_alpha = current_h - proposed_h; let u: f64 = rng.random();
if u.ln() < log_alpha && proposed_h.is_finite() {
current_state = proposed_state;
n_accepted += 1;
}
if iter >= burn_in {
samples.push(current_state.clone());
}
}
let acceptance_rate = n_accepted as f64 / n_iterations as f64;
Ok(MCMCResult {
samples,
acceptance_rate,
n_iterations,
burn_in,
})
}
}
#[derive(Debug, Clone)]
pub struct MeanFieldVariational {
pub means: Vec<f64>,
pub log_stds: Vec<f64>,
}
impl MeanFieldVariational {
pub fn new(dim: usize) -> Self {
Self {
means: vec![0.0; dim],
log_stds: vec![0.0; dim], }
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> Vec<f64> {
let mut sample = Vec::with_capacity(self.means.len());
for i in 0..self.means.len() {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
let std = self.log_stds[i].exp();
sample.push(self.means[i] + std * z);
}
sample
}
pub fn log_prob(&self, theta: &[f64]) -> f64 {
let mut log_prob = 0.0;
for i in 0..self.means.len() {
let std = self.log_stds[i].exp();
let z = (theta[i] - self.means[i]) / std;
log_prob += -0.5 * z * z - self.log_stds[i] - 0.5 * (2.0 * std::f64::consts::PI).ln();
}
log_prob
}
pub fn entropy(&self) -> f64 {
let half_log_2pi_e = 0.5 * (2.0 * std::f64::consts::PI * std::f64::consts::E).ln();
self.log_stds
.iter()
.map(|&log_std| half_log_2pi_e + log_std)
.sum()
}
}
pub struct ELBO<F, P>
where
F: Fn(&[f64]) -> f64,
P: Fn(&[f64]) -> f64,
{
log_likelihood: F,
log_prior: P,
}
impl<F, P> ELBO<F, P>
where
F: Fn(&[f64]) -> f64,
P: Fn(&[f64]) -> f64,
{
pub fn new(log_likelihood: F, log_prior: P) -> Self {
Self {
log_likelihood,
log_prior,
}
}
pub fn estimate<R: Rng>(
&self,
variational: &MeanFieldVariational,
n_samples: usize,
rng: &mut R,
) -> f64 {
let mut elbo = 0.0;
for _ in 0..n_samples {
let theta = variational.sample(rng);
let log_joint = (self.log_likelihood)(&theta) + (self.log_prior)(&theta);
let log_q = variational.log_prob(&theta);
elbo += log_joint - log_q;
}
elbo / n_samples as f64
}
pub fn estimate_gradient<R: Rng>(
&self,
variational: &MeanFieldVariational,
n_samples: usize,
rng: &mut R,
) -> (Vec<f64>, Vec<f64>) {
let dim = variational.means.len();
let mut grad_means = vec![0.0; dim];
let mut grad_log_stds = vec![0.0; dim];
for _ in 0..n_samples {
let mut epsilon = Vec::with_capacity(dim);
for _ in 0..dim {
let u1: f64 = rng.random();
let u2: f64 = rng.random();
epsilon.push((-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos());
}
let mut theta = Vec::with_capacity(dim);
for i in 0..dim {
theta.push(variational.means[i] + variational.log_stds[i].exp() * epsilon[i]);
}
let log_joint = (self.log_likelihood)(&theta) + (self.log_prior)(&theta);
let h = 1e-5;
for i in 0..dim {
let mut theta_plus = theta.clone();
theta_plus[i] += h;
let log_joint_plus =
(self.log_likelihood)(&theta_plus) + (self.log_prior)(&theta_plus);
grad_means[i] += (log_joint_plus - log_joint) / h;
grad_log_stds[i] +=
epsilon[i] * variational.log_stds[i].exp() * (log_joint_plus - log_joint) / h;
}
for i in 0..dim {
grad_log_stds[i] += 1.0; }
}
for i in 0..dim {
grad_means[i] /= n_samples as f64;
grad_log_stds[i] /= n_samples as f64;
}
(grad_means, grad_log_stds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_gaussian_proposal() {
let proposal =
GaussianProposal::new(0.5).expect("test: valid Gaussian proposal parameters");
let mut rng = thread_rng();
let current = vec![0.0, 0.0];
let proposed = proposal
.propose(¤t, &mut rng)
.expect("test: valid proposal generation");
assert_eq!(proposed.len(), 2);
assert_eq!(proposal.log_prob(¤t, &proposed), 0.0); }
#[test]
fn test_metropolis_hastings_normal() {
let log_posterior = |theta: &[f64]| -> f64 { -0.5 * theta[0].powi(2) };
let proposal =
GaussianProposal::new(0.5).expect("test: valid Gaussian proposal parameters");
let sampler = MetropolisHastings::new(log_posterior, proposal);
let mut rng = thread_rng();
let result = sampler
.sample(&[0.0], 2000, 200, &mut rng)
.expect("test: valid MCMC sampling");
assert!(!result.samples.is_empty());
assert!(result.acceptance_rate > 0.0 && result.acceptance_rate < 1.0);
let mean = result.mean();
assert!(mean[0].abs() < 0.5, "Mean {} exceeds tolerance", mean[0]);
}
#[test]
fn test_mcmc_result_statistics() {
let samples = vec![vec![1.0], vec![2.0], vec![3.0], vec![4.0], vec![5.0]];
let result = MCMCResult {
samples,
acceptance_rate: 0.5,
n_iterations: 5,
burn_in: 0,
};
let mean = result.mean();
assert_relative_eq!(mean[0], 3.0, epsilon = 1e-10);
let std = result.std();
assert_relative_eq!(std[0], 2.0_f64.sqrt(), epsilon = 1e-6);
}
#[test]
fn test_effective_sample_size() {
let samples: Vec<Vec<f64>> = (0..100).map(|i| vec![i as f64]).collect();
let result = MCMCResult {
samples,
acceptance_rate: 1.0,
n_iterations: 100,
burn_in: 0,
};
let ess = result.effective_sample_size();
assert!(ess[0] > 0.0); }
#[test]
fn test_hmc_creation() {
let log_posterior = |theta: &[f64]| -> f64 { -0.5 * theta[0].powi(2) };
let grad = |theta: &[f64]| -> Vec<f64> { vec![-theta[0]] };
let hmc = HamiltonianMC::new(log_posterior, grad, 0.1, 10);
assert!(hmc.is_ok());
let hmc_bad = HamiltonianMC::new(log_posterior, grad, -0.1, 10);
assert!(hmc_bad.is_err());
}
#[test]
fn test_hmc_sampling() {
let log_posterior = |theta: &[f64]| -> f64 { -0.5 * theta[0].powi(2) };
let grad = |theta: &[f64]| -> Vec<f64> { vec![-theta[0]] };
let hmc =
HamiltonianMC::new(log_posterior, grad, 0.1, 10).expect("test: valid HMC parameters");
let mut rng = thread_rng();
let result = hmc
.sample(&[0.0], 500, 50, &mut rng)
.expect("test: valid HMC sampling");
assert!(!result.samples.is_empty());
assert!(result.acceptance_rate > 0.0);
}
#[test]
fn test_mean_field_variational() {
let var = MeanFieldVariational::new(2);
let mut rng = thread_rng();
let sample = var.sample(&mut rng);
assert_eq!(sample.len(), 2);
let log_prob = var.log_prob(&sample);
assert!(log_prob.is_finite());
let entropy = var.entropy();
assert!(entropy > 0.0);
}
#[test]
fn test_elbo_estimation() {
let log_likelihood = |theta: &[f64]| -> f64 {
-0.5 * (1.0 - theta[0]).powi(2)
};
let log_prior = |theta: &[f64]| -> f64 {
-0.5 * theta[0].powi(2)
};
let elbo = ELBO::new(log_likelihood, log_prior);
let var = MeanFieldVariational::new(1);
let mut rng = thread_rng();
let elbo_value = elbo.estimate(&var, 2000, &mut rng);
assert!(elbo_value.is_finite(), "ELBO value is not finite");
assert!(
elbo_value < 5.0,
"ELBO value {} is unexpectedly large",
elbo_value
);
}
#[test]
fn test_elbo_gradient() {
let log_likelihood = |theta: &[f64]| -> f64 { -0.5 * (1.0 - theta[0]).powi(2) };
let log_prior = |theta: &[f64]| -> f64 { -0.5 * theta[0].powi(2) };
let elbo = ELBO::new(log_likelihood, log_prior);
let var = MeanFieldVariational::new(1);
let mut rng = thread_rng();
let (grad_means, grad_log_stds) = elbo.estimate_gradient(&var, 100, &mut rng);
assert_eq!(grad_means.len(), 1);
assert_eq!(grad_log_stds.len(), 1);
assert!(grad_means[0].is_finite());
assert!(grad_log_stds[0].is_finite());
}
}