use super::{ProposalDistribution, TargetDistribution};
use crate::error::{StatsError, StatsResult as Result};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, Axis};
use scirs2_core::validation::*;
use scirs2_core::{Rng, RngExt};
use statrs::statistics::Statistics;
use std::sync::Arc;
pub struct MultipleTryMetropolis<T: TargetDistribution, P: ProposalDistribution> {
pub target: T,
pub proposal: P,
pub current: Array1<f64>,
pub current_log_density: f64,
pub n_tries: usize,
pub n_accepted: usize,
pub n_steps: usize,
}
impl<T: TargetDistribution, P: ProposalDistribution> MultipleTryMetropolis<T, P> {
pub fn new(target: T, proposal: P, initial: Array1<f64>, ntries: usize) -> Result<Self> {
checkarray_finite(&initial, "initial")?;
check_positive(ntries, "n_tries")?;
if initial.len() != target.dim() {
return Err(StatsError::DimensionMismatch(format!(
"initial dimension ({}) must match target dimension ({})",
initial.len(),
target.dim()
)));
}
let current_log_density = target.log_density(&initial);
Ok(Self {
target,
proposal,
current: initial,
current_log_density,
n_tries: ntries,
n_accepted: 0,
n_steps: 0,
})
}
pub fn step<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<Array1<f64>> {
let mut proposals = Vec::with_capacity(self.n_tries);
let mut log_densities = Vec::with_capacity(self.n_tries);
let mut weights = Vec::with_capacity(self.n_tries);
for _ in 0..self.n_tries {
let proposal = self.proposal.sample(&self.current, rng);
let log_density = self.target.log_density(&proposal);
let weight = log_density.exp();
proposals.push(proposal);
log_densities.push(log_density);
weights.push(weight);
}
let total_weight: f64 = weights.iter().sum();
if total_weight <= 0.0 {
self.n_steps += 1;
return Ok(self.current.clone());
}
let u: f64 = rng.random();
let mut cumsum = 0.0;
let mut selected_idx = 0;
for (i, &weight) in weights.iter().enumerate() {
cumsum += weight / total_weight;
if u <= cumsum {
selected_idx = i;
break;
}
}
let selected_proposal = &proposals[selected_idx];
let selected_log_density = log_densities[selected_idx];
let mut reverse_weights = Vec::with_capacity(self.n_tries);
for _ in 0..self.n_tries {
let reverse_proposal = self.proposal.sample(selected_proposal, rng);
let reverse_log_density = self.target.log_density(&reverse_proposal);
let reverse_weight = reverse_log_density.exp();
reverse_weights.push(reverse_weight);
}
reverse_weights.push(self.current_log_density.exp());
let reverse_total_weight: f64 = reverse_weights.iter().sum();
let log_ratio = selected_log_density - self.current_log_density + reverse_total_weight.ln()
- total_weight.ln();
let accept_u: f64 = rng.random();
self.n_steps += 1;
if accept_u.ln() < log_ratio {
self.current = selected_proposal.clone();
self.current_log_density = selected_log_density;
self.n_accepted += 1;
}
Ok(self.current.clone())
}
pub fn sample<R: Rng + ?Sized>(
&mut self,
n_samples_: usize,
rng: &mut R,
) -> Result<Array2<f64>> {
let dim = self.current.len();
let mut samples = Array2::zeros((n_samples_, dim));
for i in 0..n_samples_ {
let sample = self.step(rng)?;
samples.row_mut(i).assign(&sample);
}
Ok(samples)
}
pub fn acceptance_rate(&self) -> f64 {
if self.n_steps == 0 {
0.0
} else {
self.n_accepted as f64 / self.n_steps as f64
}
}
}
pub struct ParallelTempering<
T: TargetDistribution + Clone + Send,
P: ProposalDistribution + Clone + Send,
> {
pub base_target: T,
pub proposal: P,
pub temperatures: Array1<f64>,
pub states: Vec<Array1<f64>>,
pub log_densities: Vec<f64>,
pub n_chains: usize,
pub exchange_freq: usize,
pub move_accepted: Vec<usize>,
pub exchange_accepted: Vec<usize>,
pub move_attempts: Vec<usize>,
pub exchange_attempts: Vec<usize>,
}
impl<T: TargetDistribution + Clone + Send, P: ProposalDistribution + Clone + Send>
ParallelTempering<T, P>
{
pub fn new(
base_target: T,
proposal: P,
temperatures: Array1<f64>,
initial_states: Vec<Array1<f64>>,
exchange_freq: usize,
) -> Result<Self> {
check_positive(exchange_freq, "exchange_freq")?;
let n_chains = temperatures.len();
if initial_states.len() != n_chains {
return Err(StatsError::DimensionMismatch(format!(
"initial_states length ({}) must match temperatures length ({})",
initial_states.len(),
n_chains
)));
}
for &temp in temperatures.iter() {
check_positive(temp, "temperature")?;
}
let mut log_densities = Vec::with_capacity(n_chains);
for (i, state) in initial_states.iter().enumerate() {
checkarray_finite(state, "initial_state")?;
let temp = temperatures[i];
let log_density = base_target.log_density(state) / temp;
log_densities.push(log_density);
}
Ok(Self {
base_target,
proposal,
states: initial_states,
log_densities,
temperatures,
n_chains,
exchange_freq,
move_accepted: vec![0; n_chains],
exchange_accepted: vec![0; n_chains - 1],
move_attempts: vec![0; n_chains],
exchange_attempts: vec![0; n_chains - 1],
})
}
pub fn step<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<()> {
for i in 0..self.n_chains {
let temp = self.temperatures[i];
let current_state = &self.states[i];
let proposal = self.proposal.sample(current_state, rng);
let proposal_log_density = self.base_target.log_density(&proposal) / temp;
let log_ratio = proposal_log_density - self.log_densities[i]
+ P::log_ratio(current_state, &proposal);
self.move_attempts[i] += 1;
let u: f64 = rng.random();
if u.ln() < log_ratio {
self.states[i] = proposal;
self.log_densities[i] = proposal_log_density;
self.move_accepted[i] += 1;
}
}
Ok(())
}
pub fn exchange_step<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<()> {
for i in 0..(self.n_chains - 1) {
let temp1 = self.temperatures[i];
let temp2 = self.temperatures[i + 1];
let log_density1 = self.log_densities[i];
let log_density2 = self.log_densities[i + 1];
let log_ratio = (log_density1 * temp1 - log_density2 * temp2) / temp2
- (log_density1 * temp1 - log_density2 * temp2) / temp1;
self.exchange_attempts[i] += 1;
let u: f64 = rng.random();
if u.ln() < log_ratio {
self.states.swap(i, i + 1);
let state1_new_log_density = self.base_target.log_density(&self.states[i]) / temp1;
let state2_new_log_density =
self.base_target.log_density(&self.states[i + 1]) / temp2;
self.log_densities[i] = state1_new_log_density;
self.log_densities[i + 1] = state2_new_log_density;
self.exchange_accepted[i] += 1;
}
}
Ok(())
}
pub fn sample<R: Rng + ?Sized>(
&mut self,
n_samples_: usize,
rng: &mut R,
) -> Result<Array2<f64>> {
let dim = self.states[0].len();
let mut samples = Array2::zeros((n_samples_, dim));
for i in 0..n_samples_ {
self.step(rng)?;
if i % self.exchange_freq == 0 {
self.exchange_step(rng)?;
}
let coldest_idx = self
.temperatures
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).expect("Operation failed"))
.map(|(idx, _)| idx)
.unwrap_or(0);
samples.row_mut(i).assign(&self.states[coldest_idx]);
}
Ok(samples)
}
pub fn move_acceptance_rates(&self) -> Array1<f64> {
let mut rates = Array1::zeros(self.n_chains);
for i in 0..self.n_chains {
if self.move_attempts[i] > 0 {
rates[i] = self.move_accepted[i] as f64 / self.move_attempts[i] as f64;
}
}
rates
}
pub fn exchange_acceptance_rates(&self) -> Array1<f64> {
let mut rates = Array1::zeros(self.n_chains - 1);
for i in 0..(self.n_chains - 1) {
if self.exchange_attempts[i] > 0 {
rates[i] = self.exchange_accepted[i] as f64 / self.exchange_attempts[i] as f64;
}
}
rates
}
}
pub struct SliceSampler<T: TargetDistribution> {
pub target: T,
pub current: Array1<f64>,
pub current_log_density: f64,
pub stepsize: f64,
pub max_doublings: usize,
pub n_accepted: usize,
pub n_proposed: usize,
}
impl<T: TargetDistribution> SliceSampler<T> {
pub fn new(target: T, initial: Array1<f64>, stepsize: f64) -> Result<Self> {
checkarray_finite(&initial, "initial")?;
check_positive(stepsize, "stepsize")?;
if initial.len() != target.dim() {
return Err(StatsError::DimensionMismatch(format!(
"initial dimension ({}) must match target dimension ({})",
initial.len(),
target.dim()
)));
}
let current_log_density = target.log_density(&initial);
Ok(Self {
target,
current: initial,
current_log_density,
stepsize,
max_doublings: 20,
n_accepted: 0,
n_proposed: 0,
})
}
pub fn step<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<Array1<f64>> {
let dim = self.current.len();
let mut new_state = self.current.clone();
for d in 0..dim {
new_state[d] = self.slice_sample_dimension(&new_state, d, rng)?;
}
self.current = new_state;
self.current_log_density = self.target.log_density(&self.current);
self.n_proposed += 1;
self.n_accepted += 1;
Ok(self.current.clone())
}
fn slice_sample_dimension<R: Rng + ?Sized>(
&self,
state: &Array1<f64>,
dimension: usize,
rng: &mut R,
) -> Result<f64> {
let current_value = state[dimension];
let current_log_density = self.target.log_density(state);
let u: f64 = rng.random();
let slice_level = current_log_density + u.ln();
let mut left = current_value - self.stepsize * rng.random::<f64>();
let mut right = left + self.stepsize;
for _ in 0..self.max_doublings {
let mut left_state = state.clone();
left_state[dimension] = left;
let left_log_density = self.target.log_density(&left_state);
let mut right_state = state.clone();
right_state[dimension] = right;
let right_log_density = self.target.log_density(&right_state);
if left_log_density <= slice_level && right_log_density <= slice_level {
break;
}
if rng.random::<bool>() {
left = left - (right - left);
} else {
right = right + (right - left);
}
}
loop {
let proposal = left + (right - left) * rng.random::<f64>();
let mut proposal_state = state.clone();
proposal_state[dimension] = proposal;
let proposal_log_density = self.target.log_density(&proposal_state);
if proposal_log_density > slice_level {
return Ok(proposal);
}
if proposal < current_value {
left = proposal;
} else {
right = proposal;
}
if (right - left).abs() < 1e-10 {
return Ok(current_value);
}
}
}
pub fn sample<R: Rng + ?Sized>(
&mut self,
n_samples_: usize,
rng: &mut R,
) -> Result<Array2<f64>> {
let dim = self.current.len();
let mut samples = Array2::zeros((n_samples_, dim));
for i in 0..n_samples_ {
let sample = self.step(rng)?;
samples.row_mut(i).assign(&sample);
}
Ok(samples)
}
pub fn acceptance_rate(&self) -> f64 {
1.0
}
}
pub struct EnsembleSampler<T: TargetDistribution + Clone + Send + Sync> {
pub target: Arc<T>,
pub walkers: Array2<f64>,
pub log_densities: Array1<f64>,
pub n_walkers: usize,
pub dim: usize,
pub scale: f64,
pub n_accepted: Array1<usize>,
pub n_proposed: Array1<usize>,
}
impl<T: TargetDistribution + Clone + Send + Sync> EnsembleSampler<T> {
pub fn new(target: T, initialwalkers: Array2<f64>, scale: Option<f64>) -> Result<Self> {
checkarray_finite(&initialwalkers, "initial_walkers")?;
let (n_walkers, dim) = initialwalkers.dim();
let scale = scale.unwrap_or(2.0);
if n_walkers < 2 * dim {
return Err(StatsError::InvalidArgument(format!(
"Number of walkers ({}) should be at least 2 * dim ({})",
n_walkers,
2 * dim
)));
}
check_positive(scale, "scale")?;
let mut log_densities = Array1::zeros(n_walkers);
for i in 0..n_walkers {
let walker = initialwalkers.row(i);
log_densities[i] = target.log_density(&walker.to_owned());
}
Ok(Self {
target: Arc::new(target),
walkers: initialwalkers,
log_densities,
n_walkers,
dim,
scale,
n_accepted: Array1::zeros(n_walkers),
n_proposed: Array1::zeros(n_walkers),
})
}
pub fn step<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<()> {
let n_half = self.n_walkers / 2;
self.update_group(0, n_half, n_half, self.n_walkers, rng)?;
self.update_group(n_half, self.n_walkers, 0, n_half, rng)?;
Ok(())
}
fn update_group<R: Rng + ?Sized>(
&mut self,
start: usize,
end: usize,
comp_start: usize,
comp_end: usize,
rng: &mut R,
) -> Result<()> {
for i in start..end {
let compsize = comp_end - comp_start;
let j = comp_start + rng.random_range(0..compsize);
let z = ((self.scale - 1.0) * rng.random::<f64>() + 1.0).powf(2.0) / self.scale;
let walker_i = self.walkers.row(i);
let walker_j = self.walkers.row(j);
let proposal = &walker_j.to_owned() + z * (&walker_i.to_owned() - &walker_j.to_owned());
let proposal_log_density = self.target.log_density(&proposal);
let log_ratio =
(self.dim as f64 - 1.0) * z.ln() + proposal_log_density - self.log_densities[i];
let u: f64 = rng.random();
self.n_proposed[i] += 1;
if u.ln() < log_ratio {
self.walkers.row_mut(i).assign(&proposal);
self.log_densities[i] = proposal_log_density;
self.n_accepted[i] += 1;
}
}
Ok(())
}
pub fn sample<R: Rng + ?Sized>(
&mut self,
n_samples_: usize,
rng: &mut R,
) -> Result<Array2<f64>> {
let total_samples = n_samples_ * self.n_walkers;
let mut samples = Array2::zeros((total_samples, self.dim));
for i in 0..n_samples_ {
self.step(rng)?;
for j in 0..self.n_walkers {
let sample_idx = i * self.n_walkers + j;
samples.row_mut(sample_idx).assign(&self.walkers.row(j));
}
}
Ok(samples)
}
pub fn acceptance_rates(&self) -> Array1<f64> {
let mut rates = Array1::zeros(self.n_walkers);
for i in 0..self.n_walkers {
if self.n_proposed[i] > 0 {
rates[i] = self.n_accepted[i] as f64 / self.n_proposed[i] as f64;
}
}
rates
}
pub fn get_walkers(&self) -> &Array2<f64> {
&self.walkers
}
pub fn chain_statistics(&self, samples: &Array2<f64>) -> Result<ChainStatistics> {
let (n_samples_, dim) = samples.dim();
let means = samples.mean_axis(Axis(0)).expect("Operation failed");
let mut variances = Array1::zeros(dim);
for j in 0..dim {
let col = samples.column(j);
let mean_j = means[j];
let var_j = col.mapv(|x| (x - mean_j).powi(2)).mean();
variances[j] = var_j;
}
let mut autocorr_times = Array1::zeros(dim);
for j in 0..dim {
autocorr_times[j] = self.estimate_autocorr_time(&samples.column(j))?;
}
Ok(ChainStatistics {
means,
variances,
autocorr_times,
n_samples_,
dim,
})
}
fn estimate_autocorr_time(&self, chain: &ArrayView1<f64>) -> Result<f64> {
let n = chain.len();
if n < 4 {
return Ok(1.0);
}
use scirs2_core::ndarray::ArrayStatCompat;
let mean = chain.mean_or(0.0);
let variance = chain.mapv(|x| (x - mean).powi(2)).mean_or(1.0);
if variance <= 0.0 {
return Ok(1.0);
}
let max_lag = (n / 4).min(200);
let mut autocorr = Array1::zeros(max_lag);
for lag in 0..max_lag {
let mut sum = 0.0;
let mut count = 0;
for i in 0..(n - lag) {
sum += (chain[i] - mean) * (chain[i + lag] - mean);
count += 1;
}
if count > 0 {
autocorr[lag] = sum / (count as f64 * variance);
}
}
let threshold = std::f64::consts::E.recip();
for lag in 1..max_lag {
if autocorr[lag] < threshold || autocorr[lag] < 0.0 {
return Ok(lag as f64);
}
}
Ok(max_lag as f64)
}
}
#[derive(Debug, Clone)]
pub struct ChainStatistics {
pub means: Array1<f64>,
pub variances: Array1<f64>,
pub autocorr_times: Array1<f64>,
pub n_samples_: usize,
pub dim: usize,
}
impl ChainStatistics {
pub fn effective_samplesizes(&self) -> Array1<f64> {
self.autocorr_times.mapv(|tau| {
if tau > 0.0 {
self.n_samples_ as f64 / (2.0 * tau)
} else {
self.n_samples_ as f64
}
})
}
pub fn is_converged(&self, threshold: f64) -> bool {
let max_autocorr = self.autocorr_times.iter().cloned().fold(0.0f64, f64::max);
let min_eff_samples = self.n_samples_ as f64 / (2.0 * max_autocorr);
min_eff_samples > threshold
}
}