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::runtime::handler::run;
use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
use crate::runtime::trace::{ChoiceValue, Trace};
use rand::Rng;
pub trait DistanceFunction<T> {
fn distance(&self, observed: &T, simulated: &T) -> f64;
}
pub struct EuclideanDistance;
impl DistanceFunction<Vec<f64>> for EuclideanDistance {
fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
if observed.len() != simulated.len() {
return f64::INFINITY;
}
observed
.iter()
.zip(simulated.iter())
.map(|(&o, &s)| (o - s).powi(2))
.sum::<f64>()
.sqrt()
}
}
pub struct ManhattanDistance;
impl DistanceFunction<Vec<f64>> for ManhattanDistance {
fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
if observed.len() != simulated.len() {
return f64::INFINITY;
}
observed
.iter()
.zip(simulated.iter())
.map(|(&o, &s)| (o - s).abs())
.sum::<f64>()
}
}
pub struct SummaryStatsDistance {
pub weights: Vec<f64>,
}
impl SummaryStatsDistance {
pub fn new(weights: Vec<f64>) -> Self {
Self { weights }
}
fn compute_stats(data: &[f64]) -> Vec<f64> {
if data.is_empty() {
return vec![0.0, 0.0, 0.0];
}
let mean = data.iter().sum::<f64>() / data.len() as f64;
let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
let std = variance.sqrt();
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = if sorted.len().is_multiple_of(2) {
(sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0
} else {
sorted[sorted.len() / 2]
};
vec![mean, std, median]
}
}
impl DistanceFunction<Vec<f64>> for SummaryStatsDistance {
fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
let obs_stats = Self::compute_stats(observed);
let sim_stats = Self::compute_stats(simulated);
obs_stats
.iter()
.zip(sim_stats.iter())
.zip(&self.weights)
.map(|((&o, &s), &w)| w * (o - s).powi(2))
.sum::<f64>()
.sqrt()
}
}
pub fn abc_rejection<A, T, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
simulator: impl Fn(&Trace) -> T,
observed_data: &T,
distance_fn: &dyn DistanceFunction<T>,
tolerance: f64,
max_samples: usize,
) -> Vec<Trace> {
let mut accepted = Vec::new();
let mut attempts = 0;
while accepted.len() < max_samples && attempts < max_samples * 100 {
let (_a, trace) = run(
PriorHandler {
rng,
trace: Trace::default(),
},
model_fn(),
);
let simulated_data = simulator(&trace);
let dist = distance_fn.distance(observed_data, &simulated_data);
if dist <= tolerance {
accepted.push(trace);
}
attempts += 1;
}
if accepted.is_empty() {
eprintln!(
"Warning: No samples accepted in ABC. Consider increasing tolerance or max_samples."
);
}
accepted
}
#[derive(Debug, Clone)]
pub struct ABCSMCConfig {
pub initial_tolerance: f64,
pub tolerance_schedule: Vec<f64>,
pub particles_per_round: usize,
}
pub const ABC_SMC_DEFAULT_ATTEMPT_FACTOR: usize = 100;
#[derive(Debug, Clone, PartialEq)]
pub enum ABCError {
EmptyInitialPopulation {
tolerance: f64,
attempts: usize,
},
StageExhausted {
tolerance: f64,
accepted: usize,
requested: usize,
attempts: usize,
},
}
impl std::fmt::Display for ABCError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ABCError::EmptyInitialPopulation {
tolerance,
attempts,
} => write!(
f,
"ABC-SMC initial population is empty: no draw fell within tolerance {tolerance} in {attempts} attempts"
),
ABCError::StageExhausted {
tolerance,
accepted,
requested,
attempts,
} => write!(
f,
"ABC-SMC stage at tolerance {tolerance} exhausted its budget of {attempts} attempts with only {accepted}/{requested} particles accepted"
),
}
}
}
impl std::error::Error for ABCError {}
#[derive(Debug, Clone)]
pub struct ABCParticle {
pub trace: Trace,
pub weight: f64,
}
#[derive(Debug, Clone)]
pub struct ABCSMCResult {
pub particles: Vec<ABCParticle>,
pub final_tolerance: f64,
}
impl ABCSMCResult {
pub fn weighted_mean(&self, addr: &Address) -> Option<f64> {
let mut num = 0.0;
let mut den = 0.0;
for p in &self.particles {
let v = p.trace.get_f64(addr)?;
num += p.weight * v;
den += p.weight;
}
if den > 0.0 {
Some(num / den)
} else {
None
}
}
}
pub fn abc_smc_weighted<A, T, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
simulator: impl Fn(&Trace) -> T,
observed_data: &T,
distance_fn: &dyn DistanceFunction<T>,
config: ABCSMCConfig,
max_attempts_per_stage: usize,
) -> Result<ABCSMCResult, ABCError> {
let n = config.particles_per_round;
let mut current: Vec<ABCParticle> = Vec::with_capacity(n);
let mut attempts = 0usize;
while current.len() < n && attempts < max_attempts_per_stage {
attempts += 1;
let (_a, trace) = run(
PriorHandler {
rng,
trace: Trace::default(),
},
model_fn(),
);
let dist = distance_fn.distance(observed_data, &simulator(&trace));
if dist <= config.initial_tolerance {
current.push(ABCParticle { trace, weight: 0.0 });
}
}
if current.is_empty() {
return Err(ABCError::EmptyInitialPopulation {
tolerance: config.initial_tolerance,
attempts,
});
}
let uniform = 1.0 / current.len() as f64;
for p in &mut current {
p.weight = uniform;
}
let mut current_tolerance = config.initial_tolerance;
let coord_addrs = f64_addresses(¤t[0].trace);
for &new_tolerance in &config.tolerance_schedule {
if new_tolerance >= current_tolerance {
continue; }
let kernel_std = kernel_bandwidths(¤t, &coord_addrs);
let prev_coords: Vec<Vec<f64>> = current
.iter()
.map(|p| coords_of(&p.trace, &coord_addrs))
.collect();
let prev_weights: Vec<f64> = current.iter().map(|p| p.weight).collect();
let mut next: Vec<ABCParticle> = Vec::with_capacity(n);
let mut log_weights: Vec<f64> = Vec::with_capacity(n);
let mut stage_attempts = 0usize;
while next.len() < n && stage_attempts < max_attempts_per_stage {
stage_attempts += 1;
let j = sample_index(rng, &prev_weights);
let mut proposed = current[j].trace.clone();
for (c, addr) in coord_addrs.iter().enumerate() {
if let Some(v) = proposed.get_f64(addr) {
let z = Normal::new(0.0, 1.0).unwrap().sample(rng);
let new_v = v + kernel_std[c] * z;
if let Some(choice) = proposed.choices.get_mut(addr) {
choice.value = ChoiceValue::F64(new_v);
}
}
}
let log_prior = score_log_prior(&model_fn, &proposed);
if !log_prior.is_finite() {
continue;
}
let dist = distance_fn.distance(observed_data, &simulator(&proposed));
if dist > new_tolerance {
continue;
}
let prop_coords = coords_of(&proposed, &coord_addrs);
let log_denom =
kernel_mixture_log_density(&prop_coords, &prev_coords, &prev_weights, &kernel_std);
log_weights.push(log_prior - log_denom);
next.push(ABCParticle {
trace: proposed,
weight: 0.0,
});
}
if next.is_empty() || next.len() < n {
return Err(ABCError::StageExhausted {
tolerance: new_tolerance,
accepted: next.len(),
requested: n,
attempts: max_attempts_per_stage,
});
}
let log_norm = log_sum_exp(&log_weights);
for (p, &lw) in next.iter_mut().zip(&log_weights) {
p.weight = if log_norm.is_finite() {
(lw - log_norm).exp()
} else {
1.0 / n as f64
};
}
current = next;
current_tolerance = new_tolerance;
}
Ok(ABCSMCResult {
particles: current,
final_tolerance: current_tolerance,
})
}
pub fn abc_smc<A, T, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
simulator: impl Fn(&Trace) -> T,
observed_data: &T,
distance_fn: &dyn DistanceFunction<T>,
config: ABCSMCConfig,
) -> Vec<Trace> {
let n = config.particles_per_round;
let max_attempts = ABC_SMC_DEFAULT_ATTEMPT_FACTOR.saturating_mul(n.max(1));
match abc_smc_weighted(
rng,
model_fn,
simulator,
observed_data,
distance_fn,
config,
max_attempts,
) {
Ok(result) => {
let weights: Vec<f64> = result.particles.iter().map(|p| p.weight).collect();
(0..result.particles.len())
.map(|_| result.particles[sample_index(rng, &weights)].trace.clone())
.collect()
}
Err(e) => {
eprintln!("Warning: ABC-SMC did not complete: {e}. Returning empty population.");
Vec::new()
}
}
}
fn f64_addresses(trace: &Trace) -> Vec<Address> {
trace
.choices
.iter()
.filter(|(_, c)| matches!(c.value, ChoiceValue::F64(_)))
.map(|(a, _)| a.clone())
.collect()
}
fn coords_of(trace: &Trace, addrs: &[Address]) -> Vec<f64> {
addrs
.iter()
.map(|a| trace.get_f64(a).unwrap_or(0.0))
.collect()
}
fn kernel_bandwidths(population: &[ABCParticle], addrs: &[Address]) -> Vec<f64> {
let mut std = vec![0.0; addrs.len()];
let total_w: f64 = population.iter().map(|p| p.weight).sum();
if total_w <= 0.0 {
return vec![1e-3; addrs.len()];
}
for (c, addr) in addrs.iter().enumerate() {
let mut mean = 0.0;
for p in population {
mean += p.weight * p.trace.get_f64(addr).unwrap_or(0.0);
}
mean /= total_w;
let mut var = 0.0;
for p in population {
let d = p.trace.get_f64(addr).unwrap_or(0.0) - mean;
var += p.weight * d * d;
}
var /= total_w;
let bw = (2.0 * var).sqrt();
std[c] = if bw > 1e-12 { bw } else { 1e-3 };
}
std
}
fn kernel_mixture_log_density(
x: &[f64],
centers: &[Vec<f64>],
weights: &[f64],
kernel_std: &[f64],
) -> f64 {
let terms: Vec<f64> = centers
.iter()
.zip(weights)
.map(|(center, &w)| w.ln() + gaussian_log_density(x, center, kernel_std))
.collect();
log_sum_exp(&terms)
}
fn gaussian_log_density(x: &[f64], mean: &[f64], std: &[f64]) -> f64 {
let mut lp = 0.0;
for ((&xi, &mi), &si) in x.iter().zip(mean).zip(std) {
let s = si.max(1e-12);
let z = (xi - mi) / s;
lp += -0.5 * z * z - s.ln() - 0.5 * (2.0 * std::f64::consts::PI).ln();
}
lp
}
fn score_log_prior<A>(model_fn: &impl Fn() -> Model<A>, trace: &Trace) -> f64 {
let (_a, scored) = run(
ScoreGivenTrace {
base: trace.clone(),
trace: Trace::default(),
},
model_fn(),
);
scored.log_prior
}
fn sample_index<R: Rng>(rng: &mut R, weights: &[f64]) -> usize {
let total: f64 = weights.iter().sum();
if total <= 0.0 {
return rng.gen_range(0..weights.len());
}
let u = rng.gen::<f64>() * total;
let mut cum = 0.0;
for (i, &w) in weights.iter().enumerate() {
cum += w;
if u <= cum {
return i;
}
}
weights.len() - 1
}
pub fn abc_scalar_summary<A, R: Rng>(
rng: &mut R,
model_fn: impl Fn() -> Model<A>,
simulator: impl Fn(&Trace) -> f64,
observed_summary: f64,
tolerance: f64,
max_samples: usize,
) -> Vec<Trace> {
abc_rejection(
rng,
model_fn,
|trace| vec![simulator(trace)],
&vec![observed_summary],
&EuclideanDistance,
tolerance,
max_samples,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::addr;
use crate::core::distribution::*;
use crate::core::model::sample;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn distance_functions_work() {
let eu = EuclideanDistance;
let man = ManhattanDistance;
let a = vec![1.0, 2.0, 3.0];
let b = vec![1.1, 2.1, 2.9];
let d_eu = eu.distance(&a, &b);
let d_man = man.distance(&a, &b);
assert!(d_eu > 0.0);
assert!(d_man > 0.0);
assert!(d_eu <= d_man + 1e-12);
}
#[test]
fn abc_scalar_summary_accepts_with_large_tolerance() {
let mut rng = StdRng::seed_from_u64(42);
let samples = abc_scalar_summary(
&mut rng,
|| sample(addr!("mu"), Normal::new(0.0, 2.0).unwrap()),
|trace| trace.get_f64(&addr!("mu")).unwrap_or(0.0),
0.0, 10.0, 3,
);
assert!(!samples.is_empty());
}
#[test]
fn abc_rejection_can_return_empty_with_tight_tolerance() {
let mut rng = StdRng::seed_from_u64(43);
let observed = vec![1000.0]; let res = abc_rejection(
&mut rng,
|| sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
|trace| vec![trace.get_f64(&addr!("mu")).unwrap_or(0.0)],
&observed,
&EuclideanDistance,
1e-6, 3,
);
assert!(res.is_empty());
}
#[test]
fn abc_smc_respects_tolerance_schedule() {
let mut rng = StdRng::seed_from_u64(44);
let observed = vec![0.0];
let config = ABCSMCConfig {
initial_tolerance: 2.0,
tolerance_schedule: vec![1.0, 0.5],
particles_per_round: 4,
};
let res = abc_smc(
&mut rng,
|| sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
|trace| vec![trace.get_f64(&addr!("mu")).unwrap_or(0.0)],
&observed,
&EuclideanDistance,
config,
);
assert_eq!(res.len(), 4);
}
}