use crate::core::address::Address;
use crate::core::model::Model;
use crate::core::numerical::log_sum_exp;
use crate::inference::mcmc_utils::DiminishingAdaptation;
use crate::inference::mh::{propose_and_score, SiteProposal};
use crate::runtime::handler::run;
use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
use crate::runtime::trace::Trace;
use rand::Rng;
use std::collections::HashMap;
#[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
}
}
pub trait PopulationKernel<A> {
fn sweep(
&mut self,
rng: &mut dyn rand::RngCore,
particles: &mut [Particle],
model_fn: &dyn Fn() -> Model<A>,
beta: f64,
);
}
pub struct NoKernel;
impl<A> PopulationKernel<A> for NoKernel {
fn sweep(
&mut self,
_: &mut dyn rand::RngCore,
_: &mut [Particle],
_: &dyn Fn() -> Model<A>,
_: f64,
) {
}
}
pub struct CrossoverKernel {
pub n_pairs: usize,
#[allow(clippy::type_complexity)]
pub mask: Box<dyn Fn(&Trace, &Trace, &mut dyn rand::RngCore) -> Vec<Address>>,
}
fn swap_block(a: &Trace, b: &Trace, swap: &[Address]) -> (Trace, Trace) {
let mut ca = a.clone();
let mut cb = b.clone();
for addr in swap {
let from_a = ca.choices.remove(addr);
let from_b = cb.choices.remove(addr);
if let Some(c) = from_b {
ca.choices.insert(addr.clone(), c);
}
if let Some(c) = from_a {
cb.choices.insert(addr.clone(), c);
}
}
(ca, cb)
}
impl<A> PopulationKernel<A> for CrossoverKernel {
fn sweep(
&mut self,
rng: &mut dyn rand::RngCore,
particles: &mut [Particle],
model_fn: &dyn Fn() -> Model<A>,
beta: f64,
) {
let n = particles.len();
if n < 2 {
return;
}
for _ in 0..self.n_pairs {
let i = rng.gen_range(0..n);
let mut j = rng.gen_range(0..n - 1);
if j >= i {
j += 1; }
let s = (self.mask)(&particles[i].trace, &particles[j].trace, rng);
if s.is_empty() {
continue;
}
let (ti, tj) = swap_block(&particles[i].trace, &particles[j].trace, &s);
let (_ai, ci) = run(
ScoreGivenTrace {
base: ti,
trace: Trace::default(),
},
model_fn(),
);
let (_aj, cj) = run(
ScoreGivenTrace {
base: tj,
trace: Trace::default(),
},
model_fn(),
);
let logd = |t: &Trace| t.log_prior + beta * (t.log_likelihood + t.log_factors);
let log_alpha =
(logd(&ci) + logd(&cj)) - (logd(&particles[i].trace) + logd(&particles[j].trace));
if log_alpha >= 0.0 || rng.gen::<f64>() < log_alpha.exp() {
particles[i].trace = ci; particles[j].trace = cj; }
}
}
}
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 {
adaptive_smc_with_kernel(rng, num_particles, model_fn, config, &mut NoKernel)
}
pub fn adaptive_smc_with_kernel<A, R, K>(
rng: &mut R,
num_particles: usize,
model_fn: impl Fn() -> Model<A>,
config: SMCConfig,
kernel: &mut K,
) -> SMCResult
where
R: Rng,
K: PopulationKernel<A>,
{
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,
);
}
}
kernel.sweep(
rng as &mut dyn rand::RngCore,
&mut particles,
&model_fn,
beta,
);
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 {
if current.choices.is_empty() {
return current.clone();
}
let sites: Vec<Address> = current.choices.keys().cloned().collect();
let target = sites[rng.gen_range(0..sites.len())].clone();
let scale = adaptation.get_scale(&target);
let overrides: HashMap<Address, SiteProposal> = HashMap::new();
let mut kind_cache: HashMap<Address, SiteProposal> = HashMap::new();
let (_a_prop, prop_trace, _prop_lw, lqf, lqr, _structure_changed) = propose_and_score(
rng,
model_fn,
current,
&target,
scale,
&overrides,
&mut kind_cache,
);
let (_, cur_scored) = run(
ScoreGivenTrace {
base: current.clone(),
trace: Trace::default(),
},
model_fn(),
);
let dim_term = (sites.len() as f64).ln() - (prop_trace.choices.len() as f64).ln();
let log_alpha = (prop_trace.log_prior - cur_scored.log_prior)
+ beta * (particle_log_likelihood(&prop_trace) - particle_log_likelihood(&cur_scored))
+ (lqr - lqf)
+ dim_term;
let accept = log_alpha >= 0.0 || rng.gen::<f64>() < log_alpha.exp();
adaptation.update(&target, accept);
if accept {
prop_trace
} 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
}
pub fn decode_particle<A>(particle: &Particle, model_fn: impl Fn() -> Model<A>) -> A {
let (a, _) = run(
ScoreGivenTrace {
base: particle.trace.clone(),
trace: Trace::default(),
},
model_fn(),
);
a
}
pub fn try_decode_particle<A>(
particle: &Particle,
model_fn: impl Fn() -> Model<A>,
) -> crate::error::FugueResult<A> {
let (a, scored) = run(
crate::runtime::interpreters::SafeScoreGivenTrace {
base: particle.trace.clone(),
trace: Trace::default(),
warn_on_error: false,
},
model_fn(),
);
if scored.log_prior.is_finite() {
Ok(a)
} else {
Err(crate::error::FugueError::trace_error(
"try_decode_particle",
None,
"particle trace is not a complete in-support assignment for this model",
crate::error::ErrorCode::TraceAddressNotFound,
))
}
}
pub fn decode_particles<A>(
particles: &[Particle],
model_fn: impl Fn() -> Model<A>,
) -> Vec<(A, f64)> {
particles
.iter()
.map(|p| (decode_particle(p, &model_fn), p.weight))
.collect()
}
#[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 test_smc_rejuvenation_moves_bitstring() {
let n_bits = 4usize;
let model_fn = move || {
let bits: Vec<Model<bool>> = (0..n_bits)
.map(|i| sample(addr!("bit", i), Bernoulli::new(0.5).unwrap()))
.collect();
crate::core::model::sequence_vec(bits).bind(|bs| {
let k = bs.iter().filter(|&&b| b).count() as f64;
crate::core::model::factor(k).map(move |_| bs)
})
};
let mut rng = StdRng::seed_from_u64(11);
let seed_particles = smc_prior_particles(&mut rng, 1, model_fn);
let mut particles: Vec<Particle> = (0..20).map(|_| seed_particles[0].clone()).collect();
rejuvenate_particles(&mut rng, &mut particles, model_fn, 1.0, 5);
let moved = particles.iter().any(|p| {
(0..n_bits).any(|i| {
p.trace.get_bool(&addr!("bit", i))
!= seed_particles[0].trace.get_bool(&addr!("bit", i))
})
});
assert!(
moved,
"Bool-only population did not move under rejuvenation"
);
let config = SMCConfig {
resampling_method: ResamplingMethod::Systematic,
ess_threshold: 0.5,
rejuvenation_steps: 2,
};
let result = adaptive_smc(&mut rng, 400, model_fn, config);
let p1 = std::f64::consts::E / (1.0 + std::f64::consts::E);
for i in 0..n_bits {
let mean: f64 = result
.iter()
.map(|p| {
let b = p.trace.get_bool(&addr!("bit", i)).unwrap();
p.weight * if b { 1.0 } else { 0.0 }
})
.sum();
assert!(
(mean - p1).abs() < 0.09,
"bit {} posterior mean {} vs analytic {}",
i,
mean,
p1
);
}
}
fn two_site_model(y0: f64, y1: f64) -> impl Fn() -> Model<(f64, f64)> + Clone {
move || {
sample(addr!("x", 0), Normal::new(0.0, 1.0).unwrap()).and_then(move |x0| {
sample(addr!("x", 1), Normal::new(0.0, 1.0).unwrap()).and_then(move |x1| {
observe(addr!("y", 0), Normal::new(x0, 1.0).unwrap(), y0).and_then(move |_| {
observe(addr!("y", 1), Normal::new(x1, 1.0).unwrap(), y1)
.map(move |_| (x0, x1))
})
})
})
}
}
#[allow(clippy::type_complexity)]
fn random_site_mask() -> Box<dyn Fn(&Trace, &Trace, &mut dyn rand::RngCore) -> Vec<Address>> {
Box::new(|a: &Trace, _b: &Trace, rng: &mut dyn rand::RngCore| {
a.choices
.keys()
.filter(|_| rng.gen::<bool>())
.cloned()
.collect()
})
}
#[test]
fn test_crossover_product_invariance() {
let (y0, y1) = (1.0, -0.5);
let model_fn = two_site_model(y0, y1);
let mut rng = StdRng::seed_from_u64(77);
let post0 = Normal::new(y0 / 2.0, (0.5f64).sqrt()).unwrap();
let post1 = Normal::new(y1 / 2.0, (0.5f64).sqrt()).unwrap();
let n = 300;
let mut particles: Vec<Particle> = (0..n)
.map(|_| {
let mut base = Trace::default();
base.insert_choice(
addr!("x", 0),
crate::runtime::trace::ChoiceValue::F64(post0.sample(&mut rng)),
0.0,
);
base.insert_choice(
addr!("x", 1),
crate::runtime::trace::ChoiceValue::F64(post1.sample(&mut rng)),
0.0,
);
let (_, scored) = run(
ScoreGivenTrace {
base,
trace: Trace::default(),
},
model_fn(),
);
Particle {
trace: scored,
weight: 1.0 / n as f64,
log_weight: -(n as f64).ln(),
}
})
.collect();
let mut kernel = CrossoverKernel {
n_pairs: 150,
mask: random_site_mask(),
};
for _ in 0..40 {
PopulationKernel::<(f64, f64)>::sweep(
&mut kernel,
&mut rng,
&mut particles,
&model_fn,
1.0,
);
}
for (i, target_mean) in [(0usize, y0 / 2.0), (1usize, y1 / 2.0)] {
let xs: Vec<f64> = particles
.iter()
.map(|p| p.trace.get_f64(&addr!("x", i)).unwrap())
.collect();
let mean: f64 = xs.iter().sum::<f64>() / xs.len() as f64;
let var: f64 = xs.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / xs.len() as f64;
assert!(
(mean - target_mean).abs() < 0.15,
"site {} marginal mean {} drifted from posterior {}",
i,
mean,
target_mean
);
assert!(
(var - 0.5).abs() < 0.15,
"site {} marginal var {} drifted from posterior 0.5",
i,
var
);
}
}
#[test]
fn test_crossover_preserves_uniform_weights() {
let model_fn = two_site_model(1.0, -0.5);
let mut rng = StdRng::seed_from_u64(88);
let mut particles = smc_prior_particles(&mut rng, 30, &model_fn);
let before: Vec<(f64, f64)> = particles.iter().map(|p| (p.weight, p.log_weight)).collect();
let mut kernel = CrossoverKernel {
n_pairs: 60,
mask: random_site_mask(),
};
PopulationKernel::<(f64, f64)>::sweep(
&mut kernel,
&mut rng,
&mut particles,
&model_fn,
0.7,
);
let after: Vec<(f64, f64)> = particles.iter().map(|p| (p.weight, p.log_weight)).collect();
assert_eq!(before, after, "crossover sweep modified particle weights");
}
#[test]
fn test_crossover_evidence_noncorruption() {
let (y0, y1) = (1.0, -0.5);
let model_fn = two_site_model(y0, y1);
let marg = Normal::new(0.0, (2.0f64).sqrt()).unwrap();
let analytic = marg.log_prob(&y0) + marg.log_prob(&y1);
let config = || SMCConfig {
resampling_method: ResamplingMethod::Systematic,
ess_threshold: 0.7,
rejuvenation_steps: 2,
};
let mut rng = StdRng::seed_from_u64(99);
let plain = adaptive_smc(&mut rng, 600, &model_fn, config());
let mut kernel = CrossoverKernel {
n_pairs: 300,
mask: random_site_mask(),
};
let crossed = adaptive_smc_with_kernel(&mut rng, 600, &model_fn, config(), &mut kernel);
assert!(
(plain.log_evidence - analytic).abs() < 0.25,
"NoKernel evidence {} vs analytic {}",
plain.log_evidence,
analytic
);
assert!(
(crossed.log_evidence - analytic).abs() < 0.25,
"CrossoverKernel evidence {} vs analytic {}",
crossed.log_evidence,
analytic
);
}
#[test]
fn test_crossover_support_truncation() {
let model_fn = || {
sample(addr!("x", 0), Normal::new(0.0, 1.0).unwrap()).and_then(|x0| {
sample(addr!("x", 1), Normal::new(0.0, 1.0).unwrap()).and_then(move |x1| {
crate::core::model::guard(x0 + x1 <= 1.0).map(move |_| (x0, x1))
})
})
};
let mut rng = StdRng::seed_from_u64(111);
let n = 60;
let mut particles = Vec::with_capacity(n);
while particles.len() < n {
let (_, t) = run(
PriorHandler {
rng: &mut rng,
trace: Trace::default(),
},
model_fn(),
);
if t.total_log_weight().is_finite() {
particles.push(Particle {
trace: t,
weight: 1.0 / n as f64,
log_weight: -(n as f64).ln(),
});
}
}
let mut kernel = CrossoverKernel {
n_pairs: 120,
mask: Box::new(|_: &Trace, _: &Trace, _: &mut dyn rand::RngCore| vec![addr!("x", 0)]),
};
for _ in 0..30 {
PopulationKernel::<(f64, f64)>::sweep(
&mut kernel,
&mut rng,
&mut particles,
&model_fn,
1.0,
);
for p in &particles {
let x0 = p.trace.get_f64(&addr!("x", 0)).unwrap();
let x1 = p.trace.get_f64(&addr!("x", 1)).unwrap();
assert!(
x0 + x1 <= 1.0 + 1e-12,
"accepted crossover left the truncated support: {} + {} > 1",
x0,
x1
);
}
}
}
#[test]
fn test_decode_fidelity() {
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(123);
let particles = smc_prior_particles(&mut rng, 20, model_fn);
for p in &particles {
let decoded = decode_particle(p, model_fn);
assert_eq!(decoded, p.trace.get_f64(&addr!("mu")).unwrap());
assert_eq!(try_decode_particle(p, model_fn).unwrap(), decoded);
}
let decoded = decode_particles(&particles, model_fn);
let total: f64 = decoded.iter().map(|(_, w)| w).sum();
assert!((total - 1.0).abs() < 1e-9);
let foreign = Particle {
trace: Trace::default(),
weight: 1.0,
log_weight: 0.0,
};
assert!(try_decode_particle(&foreign, model_fn).is_err());
}
#[test]
fn test_decode_weighted_mean() {
let model_fn = || {
sample(addr!("gene", 0), Normal::new(0.0, 2.0).unwrap()).and_then(|x| {
crate::core::model::factor(-0.5 * (x - 3.0) * (x - 3.0)).map(move |_| x)
})
};
let mut rng = StdRng::seed_from_u64(2024);
let config = SMCConfig {
resampling_method: ResamplingMethod::Systematic,
ess_threshold: 0.7,
rejuvenation_steps: 3,
};
let mut kernel = CrossoverKernel {
n_pairs: 200,
mask: Box::new(|_: &Trace, _: &Trace, _: &mut dyn rand::RngCore| {
vec![addr!("gene", 0)]
}),
};
let result = adaptive_smc_with_kernel(&mut rng, 800, model_fn, config, &mut kernel);
let decoded = decode_particles(&result, model_fn);
let mean: f64 = decoded.iter().map(|(x, w)| x * w).sum();
let var: f64 = decoded.iter().map(|(x, w)| w * (x - mean).powi(2)).sum();
assert!(
(mean - 2.4).abs() < 0.15,
"EV-16 posterior mean {} vs analytic 2.4",
mean
);
assert!(
(var - 0.8).abs() < 0.2,
"EV-16 posterior variance {} vs analytic 0.8",
var
);
}
#[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);
}
}