use crate::core::address::Address;
use crate::core::distribution::{Distribution, Normal};
use crate::core::model::Model;
use crate::core::numerical::log_sum_exp;
use crate::inference::mcmc_utils::DiminishingAdaptation;
use crate::runtime::handler::run;
use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
use crate::runtime::trace::{ChoiceValue, Trace};
use rand::Rng;
#[derive(Clone, Debug)]
pub struct Particle {
pub trace: Trace,
pub weight: f64,
pub log_weight: f64,
}
#[derive(Clone, Copy, Debug)]
pub enum ResamplingMethod {
Multinomial,
Systematic,
Stratified,
}
pub struct SMCConfig {
pub resampling_method: ResamplingMethod,
pub ess_threshold: f64,
pub rejuvenation_steps: usize,
}
impl Default for SMCConfig {
fn default() -> Self {
Self {
resampling_method: ResamplingMethod::Systematic,
ess_threshold: 0.5,
rejuvenation_steps: 0,
}
}
}
pub fn effective_sample_size(particles: &[Particle]) -> f64 {
let sum_sq: f64 = particles.iter().map(|p| p.weight * p.weight).sum();
1.0 / sum_sq
}
pub fn systematic_resample<R: Rng>(rng: &mut R, particles: &[Particle]) -> Vec<usize> {
systematic_indices(rng, &particle_weights(particles))
}
pub fn stratified_resample<R: Rng>(rng: &mut R, particles: &[Particle]) -> Vec<usize> {
stratified_indices(rng, &particle_weights(particles))
}
pub fn multinomial_resample<R: Rng>(rng: &mut R, particles: &[Particle]) -> Vec<usize> {
multinomial_indices(rng, &particle_weights(particles))
}
fn particle_weights(particles: &[Particle]) -> Vec<f64> {
particles.iter().map(|p| p.weight).collect()
}
fn systematic_indices<R: Rng>(rng: &mut R, weights: &[f64]) -> Vec<usize> {
let n = weights.len();
let mut indices = Vec::with_capacity(n);
let u = rng.gen::<f64>() / n as f64;
let mut cum_weight = 0.0;
let mut i = 0;
for j in 0..n {
let threshold = u + j as f64 / n as f64;
while cum_weight < threshold && i < n {
cum_weight += weights[i];
i += 1;
}
indices.push((i - 1).min(n - 1));
}
indices
}
fn stratified_indices<R: Rng>(rng: &mut R, weights: &[f64]) -> Vec<usize> {
let n = weights.len();
let mut indices = Vec::with_capacity(n);
let mut cum_weight = 0.0;
let mut i = 0;
for j in 0..n {
let u = rng.gen::<f64>();
let threshold = (j as f64 + u) / n as f64;
while cum_weight < threshold && i < n {
cum_weight += weights[i];
i += 1;
}
indices.push((i - 1).min(n - 1));
}
indices
}
fn multinomial_indices<R: Rng>(rng: &mut R, weights: &[f64]) -> Vec<usize> {
let n = weights.len();
let mut indices = Vec::with_capacity(n);
for _ in 0..n {
let u = rng.gen::<f64>();
let mut cum_weight = 0.0;
let mut selected = n - 1;
for (i, &w) in weights.iter().enumerate() {
cum_weight += w;
if u <= cum_weight {
selected = i;
break;
}
}
indices.push(selected);
}
indices
}
fn resample_indices<R: Rng>(rng: &mut R, weights: &[f64], method: ResamplingMethod) -> Vec<usize> {
match method {
ResamplingMethod::Multinomial => multinomial_indices(rng, weights),
ResamplingMethod::Systematic => systematic_indices(rng, weights),
ResamplingMethod::Stratified => stratified_indices(rng, weights),
}
}
pub fn resample_particles<R: Rng>(
rng: &mut R,
particles: &[Particle],
method: ResamplingMethod,
) -> Vec<Particle> {
let indices = match method {
ResamplingMethod::Multinomial => multinomial_resample(rng, particles),
ResamplingMethod::Systematic => systematic_resample(rng, particles),
ResamplingMethod::Stratified => stratified_resample(rng, particles),
};
let n = particles.len();
let uniform_weight = 1.0 / n as f64;
indices
.into_iter()
.map(|i| {
let mut p = particles[i].clone();
p.weight = uniform_weight;
p.log_weight = uniform_weight.ln();
p
})
.collect()
}
#[derive(Clone, Debug)]
pub struct SMCResult {
pub particles: Vec<Particle>,
pub log_evidence: f64,
}
impl std::ops::Deref for SMCResult {
type Target = Vec<Particle>;
fn deref(&self) -> &Self::Target {
&self.particles
}
}
fn particle_log_likelihood(trace: &Trace) -> f64 {
trace.log_likelihood + trace.log_factors
}
pub fn adaptive_smc<A, R: Rng>(
rng: &mut R,
num_particles: usize,
model_fn: impl Fn() -> Model<A>,
config: SMCConfig,
) -> SMCResult {
let n = num_particles;
if n == 0 {
return SMCResult {
particles: Vec::new(),
log_evidence: 0.0,
};
}
let mut particles = smc_prior_particles(rng, n, &model_fn);
let mut logliks: Vec<f64> = particles
.iter()
.map(|p| particle_log_likelihood(&p.trace))
.collect();
let mut log_w = vec![-(n as f64).ln(); n];
let mut beta = 0.0_f64;
let mut log_evidence = 0.0_f64;
let target_ess = (config.ess_threshold * n as f64).clamp(1.0, n as f64);
let mut adaptation = DiminishingAdaptation::new(0.44, 0.7);
if config.rejuvenation_steps == 0 {
let combined: Vec<f64> = logliks.iter().map(|ll| -(n as f64).ln() + ll).collect();
log_evidence = log_sum_exp(&combined);
beta = 1.0;
log_w = combined;
} else {
const MAX_STEPS: usize = 10_000;
let mut steps = 0;
while beta < 1.0 {
steps += 1;
let mut beta_new = next_beta(beta, &log_w, &logliks, target_ess);
if steps >= MAX_STEPS {
beta_new = 1.0;
}
let d_beta = beta_new - beta;
let combined: Vec<f64> = log_w
.iter()
.zip(&logliks)
.map(|(lw, ll)| lw + d_beta * ll)
.collect();
let log_norm = log_sum_exp(&combined);
log_evidence += log_norm;
if log_norm.is_finite() {
for (lw, c) in log_w.iter_mut().zip(&combined) {
*lw = c - log_norm;
}
} else {
for lw in log_w.iter_mut() {
*lw = -(n as f64).ln();
}
}
beta = beta_new;
if beta < 1.0 {
let weights: Vec<f64> = log_w.iter().map(|lw| lw.exp()).collect();
let indices = resample_indices(rng, &weights, config.resampling_method);
particles = indices.iter().map(|&i| particles[i].clone()).collect();
for lw in log_w.iter_mut() {
*lw = -(n as f64).ln();
}
for particle in particles.iter_mut() {
for _ in 0..config.rejuvenation_steps {
particle.trace = tempered_single_site_mh(
rng,
&model_fn,
&particle.trace,
beta,
&mut adaptation,
);
}
}
logliks = particles
.iter()
.map(|p| particle_log_likelihood(&p.trace))
.collect();
}
}
}
let _ = beta;
let log_norm = log_sum_exp(&log_w);
for (p, &lw) in particles.iter_mut().zip(&log_w) {
if log_norm.is_finite() {
let normalized = lw - log_norm;
p.log_weight = normalized;
p.weight = normalized.exp();
} else {
p.log_weight = -(n as f64).ln();
p.weight = 1.0 / n as f64;
}
}
SMCResult {
particles,
log_evidence,
}
}
fn next_beta(beta: f64, log_w: &[f64], logliks: &[f64], target_ess: f64) -> f64 {
let ess_at = |b: f64| -> f64 {
let lv: Vec<f64> = log_w
.iter()
.zip(logliks)
.map(|(lw, ll)| lw + (b - beta) * ll)
.collect();
let lse1 = log_sum_exp(&lv);
let lv2: Vec<f64> = lv.iter().map(|x| 2.0 * x).collect();
let lse2 = log_sum_exp(&lv2);
if !lse1.is_finite() || !lse2.is_finite() {
return log_w.len() as f64;
}
(2.0 * lse1 - lse2).exp()
};
if ess_at(1.0) >= target_ess {
return 1.0;
}
let mut lo = beta;
let mut hi = 1.0;
for _ in 0..64 {
let mid = 0.5 * (lo + hi);
if ess_at(mid) < target_ess {
hi = mid;
} else {
lo = mid;
}
}
hi.max(beta + 1e-9).min(1.0)
}
fn tempered_single_site_mh<A, R: Rng>(
rng: &mut R,
model_fn: &impl Fn() -> Model<A>,
current: &Trace,
beta: f64,
adaptation: &mut DiminishingAdaptation,
) -> Trace {
let f64_sites: Vec<Address> = current
.choices
.iter()
.filter(|(_, c)| matches!(c.value, ChoiceValue::F64(_)))
.map(|(a, _)| a.clone())
.collect();
if f64_sites.is_empty() {
return current.clone();
}
let site = f64_sites[rng.gen_range(0..f64_sites.len())].clone();
let scale = adaptation.get_scale(&site);
let cur_val = current.choices[&site].value.as_f64().unwrap();
let z = Normal::new(0.0, 1.0).unwrap().sample(rng);
let prop_val = cur_val + scale * z;
let mut proposed = current.clone();
proposed.choices.get_mut(&site).unwrap().value = ChoiceValue::F64(prop_val);
let (_, cur_scored) = run(
ScoreGivenTrace {
base: current.clone(),
trace: Trace::default(),
},
model_fn(),
);
let (_, prop_scored) = run(
ScoreGivenTrace {
base: proposed,
trace: Trace::default(),
},
model_fn(),
);
let log_alpha = (prop_scored.log_prior - cur_scored.log_prior)
+ beta * (particle_log_likelihood(&prop_scored) - particle_log_likelihood(&cur_scored));
let accept = log_alpha >= 0.0 || rng.gen::<f64>() < log_alpha.exp();
adaptation.update(&site, accept);
if accept {
prop_scored
} else {
cur_scored
}
}
pub fn rejuvenate_particles<A, R: Rng>(
rng: &mut R,
particles: &mut [Particle],
model_fn: impl Fn() -> Model<A>,
beta: f64,
rejuvenation_steps: usize,
) {
let mut adaptation = DiminishingAdaptation::new(0.44, 0.7);
for particle in particles.iter_mut() {
for _ in 0..rejuvenation_steps {
particle.trace =
tempered_single_site_mh(rng, &model_fn, &particle.trace, beta, &mut adaptation);
}
}
}
pub fn normalize_particles(particles: &mut [Particle]) {
use crate::core::numerical::log_sum_exp;
if particles.is_empty() {
return;
}
let log_weights: Vec<f64> = particles.iter().map(|p| p.log_weight).collect();
let log_norm = log_sum_exp(&log_weights);
if log_norm.is_infinite() && log_norm < 0.0 {
let n = particles.len();
for p in particles {
p.weight = 1.0 / n as f64; }
return;
}
for (p, &log_w) in particles.iter_mut().zip(&log_weights) {
p.weight = (log_w - log_norm).exp();
}
let weight_sum: f64 = particles.iter().map(|p| p.weight).sum();
if weight_sum > 0.0 {
for p in particles {
p.weight /= weight_sum;
}
}
}
pub fn smc_prior_particles<A, R: Rng>(
rng: &mut R,
num_particles: usize,
model_fn: impl Fn() -> Model<A>,
) -> Vec<Particle> {
let mut particles = Vec::with_capacity(num_particles);
for _ in 0..num_particles {
let (_a, t) = run(
PriorHandler {
rng,
trace: Trace::default(),
},
model_fn(),
);
let log_weight = particle_log_likelihood(&t);
particles.push(Particle {
trace: t,
weight: 0.0, log_weight,
});
}
normalize_particles(&mut particles);
particles
}
#[cfg(test)]
mod tests {
use super::*;
use crate::addr;
use crate::core::distribution::*;
use crate::core::model::{observe, sample, ModelExt};
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn ess_and_resampling_behave() {
let particles = vec![
Particle {
trace: Trace::default(),
weight: 0.7,
log_weight: (0.7f64).ln(),
},
Particle {
trace: Trace::default(),
weight: 0.2,
log_weight: (0.2f64).ln(),
},
Particle {
trace: Trace::default(),
weight: 0.09,
log_weight: (0.09f64).ln(),
},
Particle {
trace: Trace::default(),
weight: 0.01,
log_weight: (0.01f64).ln(),
},
];
let ess_val = effective_sample_size(&particles);
assert!(ess_val < particles.len() as f64);
let mut rng = StdRng::seed_from_u64(1);
let idx_m = multinomial_resample(&mut rng, &particles);
assert_eq!(idx_m.len(), particles.len());
let idx_s = systematic_resample(&mut rng, &particles);
assert_eq!(idx_s.len(), particles.len());
let idx_t = stratified_resample(&mut rng, &particles);
assert_eq!(idx_t.len(), particles.len());
let resampled = resample_particles(&mut rng, &particles, ResamplingMethod::Systematic);
let sum_w: f64 = resampled.iter().map(|p| p.weight).sum();
assert!((sum_w - 1.0).abs() < 1e-12);
for p in &resampled {
assert!((p.weight - 0.25).abs() < 1e-12);
}
}
#[test]
fn normalize_particles_handles_neg_inf() {
let mut particles = vec![
Particle {
trace: Trace::default(),
weight: 0.0,
log_weight: f64::NEG_INFINITY,
},
Particle {
trace: Trace::default(),
weight: 0.0,
log_weight: f64::NEG_INFINITY,
},
];
normalize_particles(&mut particles);
assert!((particles[0].weight - 0.5).abs() < 1e-12);
assert!((particles[1].weight - 0.5).abs() < 1e-12);
}
#[test]
fn adaptive_smc_runs_with_small_config() {
let model_fn = || {
sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()).and_then(|mu| {
observe(addr!("y"), Normal::new(mu, 1.0).unwrap(), 0.5).map(move |_| mu)
})
};
let mut rng = StdRng::seed_from_u64(2);
let config = SMCConfig {
resampling_method: ResamplingMethod::Systematic,
ess_threshold: 0.5,
rejuvenation_steps: 1,
};
let particles = adaptive_smc(&mut rng, 5, model_fn, config);
assert_eq!(particles.len(), 5);
let sum_w: f64 = particles.iter().map(|p| p.weight).sum();
assert!((sum_w - 1.0).abs() < 1e-9);
}
}