Skip to main content

fugue/inference/
abc.rs

1//! Approximate Bayesian Computation (ABC) - likelihood-free inference methods.
2//!
3//! ABC methods enable Bayesian inference for models where the likelihood function
4//! is intractable or unavailable, but forward simulation from the model is possible.
5//! Instead of computing likelihoods directly, ABC compares simulated data to observed
6//! data using distance functions and accepts samples that produce "similar" outcomes.
7//!
8//! ## Method Overview
9//!
10//! ABC algorithms follow this general pattern:
11//! 1. Sample parameters from the prior distribution
12//! 2. Simulate data using the model with those parameters
13//! 3. Compare simulated data to observed data using a distance function
14//! 4. Accept samples where the distance is below a threshold ε
15//!
16//! As ε → 0, the ABC posterior approaches the true posterior distribution.
17//!
18//! ## Available Methods
19//!
20//! - [`abc_rejection`]: Basic rejection ABC
21//! - [`abc_smc`]: Sequential Monte Carlo ABC for improved efficiency
22//! - [`abc_scalar_summary`]: ABC with scalar summary statistics
23//!
24//! ## Distance Functions
25//!
26//! The quality of ABC inference depends heavily on the choice of distance function:
27//! - [`EuclideanDistance`]: L2 norm for continuous data vectors
28//! - [`ManhattanDistance`]: L1 norm for robust distance computation
29//! - Custom distance functions via the [`DistanceFunction`] trait
30//!
31//! # Examples
32//!
33//! ```rust
34//! use fugue::*;
35//! use rand::rngs::StdRng;
36//! use rand::SeedableRng;
37//! use rand::Rng;
38//!
39//! // Simple ABC example for illustration
40//! let mut rng = StdRng::seed_from_u64(42);
41//! let observed_data = vec![2.0];
42//!
43//! let samples = abc_scalar_summary(
44//!     &mut rng,
45//!     || sample(addr!("mu"), Normal::new(0.0, 2.0).unwrap()),
46//!     |trace| {
47//!         if let Some(choice) = trace.choices.get(&addr!("mu")) {
48//!             if let ChoiceValue::F64(mu) = choice.value {
49//!                 mu
50//!             } else { 0.0 }
51//!         } else { 0.0 }
52//!     },
53//!     2.0, // observed summary
54//!     0.5, // tolerance
55//!     10   // max samples
56//! );
57//!
58//! assert!(!samples.is_empty());
59//! ```
60
61use crate::core::address::Address;
62use crate::core::distribution::{Distribution, Normal};
63use crate::core::model::Model;
64use crate::core::numerical::log_sum_exp;
65use crate::runtime::handler::run;
66use crate::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
67use crate::runtime::trace::{ChoiceValue, Trace};
68use rand::Rng;
69
70/// Trait for computing distances between observed and simulated data.
71///
72/// Distance functions are crucial for ABC methods as they determine how
73/// "similarity" between datasets is measured. The choice of distance function
74/// significantly affects the quality of ABC approximations.
75///
76/// # Type Parameter
77///
78/// * `T` - Type of data being compared (e.g., `Vec<f64>`, scalar values)
79///
80/// # Examples
81///
82/// ```rust
83/// use fugue::*;
84///
85/// // Use built-in Euclidean distance
86/// let euclidean = EuclideanDistance;
87/// let dist = euclidean.distance(&vec![1.0, 2.0], &vec![1.1, 2.1]);
88///
89/// // Implement custom distance function
90/// struct ScalarDistance;
91/// impl DistanceFunction<f64> for ScalarDistance {
92///     fn distance(&self, observed: &f64, simulated: &f64) -> f64 {
93///         (observed - simulated).abs()
94///     }
95/// }
96/// ```
97pub trait DistanceFunction<T> {
98    /// Compute the distance between observed and simulated data.
99    ///
100    /// # Arguments
101    ///
102    /// * `observed` - The actual observed data
103    /// * `simulated` - Data simulated from the model
104    ///
105    /// # Returns
106    ///
107    /// A non-negative distance value. Smaller values indicate greater similarity.
108    fn distance(&self, observed: &T, simulated: &T) -> f64;
109}
110
111/// Euclidean (L2) distance function for vector data.
112///
113/// Computes the standard Euclidean distance between two vectors:
114/// √(Σ(xᵢ - yᵢ)²)
115///
116/// This is appropriate for continuous data where the magnitude of differences
117/// matters and the data dimensions have similar scales.
118///
119/// # Examples
120///
121/// ```rust
122/// use fugue::*;
123///
124/// let euclidean = EuclideanDistance;
125/// let observed = vec![1.0, 2.0, 3.0];
126/// let simulated = vec![1.1, 2.1, 2.9];
127/// let distance = euclidean.distance(&observed, &simulated);
128/// assert!((distance - 0.173).abs() < 0.01); // ≈ 0.173
129/// ```
130pub struct EuclideanDistance;
131
132impl DistanceFunction<Vec<f64>> for EuclideanDistance {
133    fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
134        if observed.len() != simulated.len() {
135            return f64::INFINITY;
136        }
137
138        observed
139            .iter()
140            .zip(simulated.iter())
141            .map(|(&o, &s)| (o - s).powi(2))
142            .sum::<f64>()
143            .sqrt()
144    }
145}
146
147/// Manhattan (L1) distance function for vector data.
148///
149/// Computes the Manhattan distance between two vectors:
150/// Σ|xᵢ - yᵢ|
151///
152/// This distance is more robust to outliers than Euclidean distance and is
153/// appropriate when you want to treat each dimension independently.
154///
155/// # Examples
156///
157/// ```rust
158/// use fugue::inference::abc::{ManhattanDistance, DistanceFunction};
159///
160/// let manhattan = ManhattanDistance;
161/// let observed = vec![1.0, 2.0, 3.0];
162/// let simulated = vec![1.5, 1.5, 3.5];
163/// let distance = manhattan.distance(&observed, &simulated);
164/// assert!((distance - 1.5).abs() < 0.001); // |1.0-1.5| + |2.0-1.5| + |3.0-3.5| = 0.5 + 0.5 + 0.5 = 1.5
165/// ```
166pub struct ManhattanDistance;
167
168impl DistanceFunction<Vec<f64>> for ManhattanDistance {
169    fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
170        if observed.len() != simulated.len() {
171            return f64::INFINITY;
172        }
173
174        observed
175            .iter()
176            .zip(simulated.iter())
177            .map(|(&o, &s)| (o - s).abs())
178            .sum::<f64>()
179    }
180}
181
182/// Summary statistics distance.
183pub struct SummaryStatsDistance {
184    pub weights: Vec<f64>,
185}
186
187impl SummaryStatsDistance {
188    pub fn new(weights: Vec<f64>) -> Self {
189        Self { weights }
190    }
191
192    fn compute_stats(data: &[f64]) -> Vec<f64> {
193        if data.is_empty() {
194            return vec![0.0, 0.0, 0.0];
195        }
196
197        let mean = data.iter().sum::<f64>() / data.len() as f64;
198        let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / data.len() as f64;
199        let std = variance.sqrt();
200
201        let mut sorted = data.to_vec();
202        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
203        let median = if sorted.len().is_multiple_of(2) {
204            (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2.0
205        } else {
206            sorted[sorted.len() / 2]
207        };
208
209        vec![mean, std, median]
210    }
211}
212
213impl DistanceFunction<Vec<f64>> for SummaryStatsDistance {
214    fn distance(&self, observed: &Vec<f64>, simulated: &Vec<f64>) -> f64 {
215        let obs_stats = Self::compute_stats(observed);
216        let sim_stats = Self::compute_stats(simulated);
217
218        obs_stats
219            .iter()
220            .zip(sim_stats.iter())
221            .zip(&self.weights)
222            .map(|((&o, &s), &w)| w * (o - s).powi(2))
223            .sum::<f64>()
224            .sqrt()
225    }
226}
227
228/// Basic ABC rejection sampling algorithm.
229///
230/// The simplest ABC method: repeatedly sample from the prior, simulate data,
231/// and accept samples where the distance to observed data is below a tolerance.
232/// This method is straightforward but can be inefficient for small tolerances.
233///
234/// # Algorithm
235///
236/// 1. Sample parameters from the prior using `model_fn()`
237/// 2. Simulate data using `simulator(trace)`
238/// 3. Compute distance between simulated and observed data
239/// 4. Accept if distance ≤ tolerance
240/// 5. Repeat until `max_samples` accepted or too many attempts
241///
242/// # Arguments
243///
244/// * `rng` - Random number generator
245/// * `model_fn` - Function that creates a model instance (contains priors)
246/// * `simulator` - Function that simulates data given a trace of parameter values
247/// * `observed_data` - The actual observed data to match
248/// * `distance_fn` - Function for measuring similarity between datasets
249/// * `tolerance` - Maximum allowed distance for acceptance
250/// * `max_samples` - Maximum number of samples to accept
251///
252/// # Returns
253///
254/// Vector of accepted traces (parameter samples that produced similar data).
255///
256/// # Examples
257///
258/// ```rust
259/// use fugue::*;
260/// use rand::rngs::StdRng;
261/// use rand::SeedableRng;
262///
263/// // Simple ABC rejection example
264/// let mut rng = StdRng::seed_from_u64(42);
265/// let observed_data = vec![2.0];
266///
267/// let samples = abc_scalar_summary(
268///     &mut rng,
269///     || sample(addr!("mu"), Normal::new(0.0, 2.0).unwrap()),
270///     |trace| {
271///         if let Some(choice) = trace.choices.get(&addr!("mu")) {
272///             if let ChoiceValue::F64(mu) = choice.value {
273///                 mu
274///             } else { 0.0 }
275///         } else { 0.0 }
276///     },
277///     2.0, // observed summary
278///     0.5, // tolerance
279///     5    // max samples (small for test)
280/// );
281/// assert!(!samples.is_empty());
282/// ```
283pub fn abc_rejection<A, T, R: Rng>(
284    rng: &mut R,
285    model_fn: impl Fn() -> Model<A>,
286    simulator: impl Fn(&Trace) -> T,
287    observed_data: &T,
288    distance_fn: &dyn DistanceFunction<T>,
289    tolerance: f64,
290    max_samples: usize,
291) -> Vec<Trace> {
292    let mut accepted = Vec::new();
293    let mut attempts = 0;
294
295    while accepted.len() < max_samples && attempts < max_samples * 100 {
296        // Sample from prior
297        let (_a, trace) = run(
298            PriorHandler {
299                rng,
300                trace: Trace::default(),
301            },
302            model_fn(),
303        );
304
305        // Simulate data
306        let simulated_data = simulator(&trace);
307
308        // Check distance
309        let dist = distance_fn.distance(observed_data, &simulated_data);
310
311        if dist <= tolerance {
312            accepted.push(trace);
313        }
314
315        attempts += 1;
316    }
317
318    if accepted.is_empty() {
319        eprintln!(
320            "Warning: No samples accepted in ABC. Consider increasing tolerance or max_samples."
321        );
322    }
323
324    accepted
325}
326
327/// Sequential Monte Carlo ABC with adaptive tolerance scheduling.
328///
329/// An importance-weighted ABC-SMC (Beaumont 2009 / Toni et al. 2009) that
330/// iteratively reduces the tolerance, giving better posterior approximations than
331/// rejection ABC at stringent tolerances. See [`abc_smc`] (equally-weighted
332/// population) and [`abc_smc_weighted`] (weighted population with typed errors).
333///
334/// # Algorithm
335///
336/// 1. Start with the initial tolerance and generate a population using rejection ABC.
337/// 2. For each subsequent tolerance level:
338///    - draw a base particle from the previous population proportional to its
339///      importance weight,
340///    - perturb its continuous coordinates with a Gaussian kernel scaled by the
341///      weighted sample variance,
342///    - reject out-of-support proposals and accept those within the new tolerance,
343///    - weight each accepted particle by `pi(theta) / sum_j w_j K(theta | theta_j)`.
344/// 3. Final particles approximate the posterior at the strictest tolerance.
345///
346/// # Arguments
347///
348/// * `rng` - Random number generator
349/// * `model_fn` - Function that creates a model instance
350/// * `simulator` - Function that simulates data given a trace
351/// * `observed_data` - The observed data to match
352/// * `distance_fn` - Distance function for comparing datasets
353/// * `config` - Initial tolerance, decreasing tolerance schedule, population size
354///
355/// # Returns
356///
357/// Vector of traces from the final SMC population.
358///
359/// # Examples
360///
361/// ```rust
362/// use fugue::{inference::abc::ABCSMCConfig, *};
363/// use rand::rngs::StdRng;
364/// use rand::SeedableRng;
365///
366/// // Simple SMC-ABC example with small numbers for testing
367/// let observed = vec![2.0];
368/// let mut rng = StdRng::seed_from_u64(42);
369///
370/// let samples = abc_smc(
371///     &mut rng,
372///     || sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
373///     |trace| {
374///         if let Some(choice) = trace.choices.get(&addr!("mu")) {
375///             if let ChoiceValue::F64(mu) = choice.value {
376///                 vec![mu]
377///             } else { vec![0.0] }
378///         } else { vec![0.0] }
379///     },
380///     &observed,
381///     &EuclideanDistance,
382///     ABCSMCConfig {
383///         initial_tolerance: 1.0,
384///         tolerance_schedule: vec![0.5],
385///         particles_per_round: 5,
386///     },
387/// );
388/// assert!(!samples.is_empty());
389/// ```
390/// Configuration for ABC-SMC algorithm.
391#[derive(Debug, Clone)]
392pub struct ABCSMCConfig {
393    /// Initial tolerance for distance threshold
394    pub initial_tolerance: f64,
395    /// Schedule of decreasing tolerances across rounds  
396    pub tolerance_schedule: Vec<f64>,
397    /// Number of particles to generate per round
398    pub particles_per_round: usize,
399}
400
401/// Default per-stage attempt budget as a multiple of the population size,
402/// mirroring the `max_samples * 100` bound used by [`abc_rejection`].
403pub const ABC_SMC_DEFAULT_ATTEMPT_FACTOR: usize = 100;
404
405/// Errors that can occur during a bounded ABC-SMC run (finding FG-34).
406#[derive(Debug, Clone, PartialEq)]
407pub enum ABCError {
408    /// The initial rejection round accepted zero particles within its attempt
409    /// budget, so there is nothing to perturb. Previously this panicked in
410    /// `rng.gen_range(0..0)`.
411    EmptyInitialPopulation {
412        /// The initial tolerance that admitted no samples.
413        tolerance: f64,
414        /// Number of prior draws attempted before giving up.
415        attempts: usize,
416    },
417    /// A tolerance stage could not be filled within its attempt budget.
418    /// Previously the inner loop had no cap and could spin forever.
419    StageExhausted {
420        /// The tolerance level that could not be reached.
421        tolerance: f64,
422        /// Number of particles accepted before the budget was exhausted.
423        accepted: usize,
424        /// Number of particles requested for the stage.
425        requested: usize,
426        /// Attempt budget that was exhausted.
427        attempts: usize,
428    },
429}
430
431impl std::fmt::Display for ABCError {
432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433        match self {
434            ABCError::EmptyInitialPopulation {
435                tolerance,
436                attempts,
437            } => write!(
438                f,
439                "ABC-SMC initial population is empty: no draw fell within tolerance {tolerance} in {attempts} attempts"
440            ),
441            ABCError::StageExhausted {
442                tolerance,
443                accepted,
444                requested,
445                attempts,
446            } => write!(
447                f,
448                "ABC-SMC stage at tolerance {tolerance} exhausted its budget of {attempts} attempts with only {accepted}/{requested} particles accepted"
449            ),
450        }
451    }
452}
453
454impl std::error::Error for ABCError {}
455
456/// A weighted ABC-SMC particle: a parameter trace with its importance weight.
457#[derive(Debug, Clone)]
458pub struct ABCParticle {
459    /// The accepted parameter trace.
460    pub trace: Trace,
461    /// Normalized importance weight within the population.
462    pub weight: f64,
463}
464
465/// Result of a correct ABC-SMC run: a weighted posterior population.
466#[derive(Debug, Clone)]
467pub struct ABCSMCResult {
468    /// The final weighted particle population (weights sum to 1).
469    pub particles: Vec<ABCParticle>,
470    /// The tolerance level of the final population.
471    pub final_tolerance: f64,
472}
473
474impl ABCSMCResult {
475    /// Weighted posterior mean of the f64 value at `addr`, if present.
476    pub fn weighted_mean(&self, addr: &Address) -> Option<f64> {
477        let mut num = 0.0;
478        let mut den = 0.0;
479        for p in &self.particles {
480            let v = p.trace.get_f64(addr)?;
481            num += p.weight * v;
482            den += p.weight;
483        }
484        if den > 0.0 {
485            Some(num / den)
486        } else {
487            None
488        }
489    }
490}
491
492/// Sequential Monte Carlo ABC (Beaumont 2009 / Toni et al. 2009).
493///
494/// This is the correct, importance-weighted ABC-SMC. It fixes finding FG-09:
495/// each population after the first is generated by
496///
497/// 1. drawing a base particle from the previous population *proportional to its
498///    importance weight*,
499/// 2. perturbing its continuous coordinates with a Gaussian kernel whose
500///    per-component bandwidth is `sqrt(2 · weighted-variance)` of the previous
501///    population (Beaumont et al. 2009),
502/// 3. rejecting proposals with zero prior density (out of support), and
503/// 4. accepting proposals within the new tolerance, then weighting each accepted
504///    particle by `w_i ∝ π(θ_i) / Σ_j w_j K(θ_i | θ_j)` — the prior/kernel
505///    correction that the previous single-site prior-replacement heuristic
506///    omitted.
507///
508/// Each stage is bounded by `max_attempts_per_stage` attempts (finding FG-34);
509/// an empty initial population and an exhausted stage are reported as typed
510/// [`ABCError`]s instead of panicking / looping forever.
511///
512/// The perturbation kernel acts on the model's continuous (f64) sites; discrete
513/// sites are carried through from the base particle unchanged, so the importance
514/// correction is exact for continuous parameters.
515///
516/// # Returns
517///
518/// An [`ABCSMCResult`] with the weighted posterior population at the final
519/// tolerance, or an [`ABCError`] if a stage could not be completed.
520pub fn abc_smc_weighted<A, T, R: Rng>(
521    rng: &mut R,
522    model_fn: impl Fn() -> Model<A>,
523    simulator: impl Fn(&Trace) -> T,
524    observed_data: &T,
525    distance_fn: &dyn DistanceFunction<T>,
526    config: ABCSMCConfig,
527    max_attempts_per_stage: usize,
528) -> Result<ABCSMCResult, ABCError> {
529    let n = config.particles_per_round;
530
531    // ----- Population 0: bounded rejection ABC at the initial tolerance -----
532    let mut current: Vec<ABCParticle> = Vec::with_capacity(n);
533    let mut attempts = 0usize;
534    while current.len() < n && attempts < max_attempts_per_stage {
535        attempts += 1;
536        let (_a, trace) = run(
537            PriorHandler {
538                rng,
539                trace: Trace::default(),
540            },
541            model_fn(),
542        );
543        let dist = distance_fn.distance(observed_data, &simulator(&trace));
544        if dist <= config.initial_tolerance {
545            current.push(ABCParticle { trace, weight: 0.0 });
546        }
547    }
548    if current.is_empty() {
549        return Err(ABCError::EmptyInitialPopulation {
550            tolerance: config.initial_tolerance,
551            attempts,
552        });
553    }
554    // Population 0 carries uniform weights.
555    let uniform = 1.0 / current.len() as f64;
556    for p in &mut current {
557        p.weight = uniform;
558    }
559    let mut current_tolerance = config.initial_tolerance;
560
561    // Continuous coordinate addresses shared by the population.
562    let coord_addrs = f64_addresses(&current[0].trace);
563
564    // ----- Sequential rounds with decreasing tolerance -----
565    for &new_tolerance in &config.tolerance_schedule {
566        if new_tolerance >= current_tolerance {
567            continue; // Skip non-decreasing tolerances.
568        }
569
570        // Kernel bandwidth per continuous component: sqrt(2 * weighted variance).
571        let kernel_std = kernel_bandwidths(&current, &coord_addrs);
572        let prev_coords: Vec<Vec<f64>> = current
573            .iter()
574            .map(|p| coords_of(&p.trace, &coord_addrs))
575            .collect();
576        let prev_weights: Vec<f64> = current.iter().map(|p| p.weight).collect();
577
578        let mut next: Vec<ABCParticle> = Vec::with_capacity(n);
579        let mut log_weights: Vec<f64> = Vec::with_capacity(n);
580        let mut stage_attempts = 0usize;
581
582        while next.len() < n && stage_attempts < max_attempts_per_stage {
583            stage_attempts += 1;
584
585            // (1) Draw a base particle proportional to its importance weight.
586            let j = sample_index(rng, &prev_weights);
587            let mut proposed = current[j].trace.clone();
588
589            // (2) Perturb continuous coordinates with the Gaussian kernel.
590            for (c, addr) in coord_addrs.iter().enumerate() {
591                if let Some(v) = proposed.get_f64(addr) {
592                    let z = Normal::new(0.0, 1.0).unwrap().sample(rng);
593                    let new_v = v + kernel_std[c] * z;
594                    if let Some(choice) = proposed.choices.get_mut(addr) {
595                        choice.value = ChoiceValue::F64(new_v);
596                    }
597                }
598            }
599
600            // (3) Reject proposals with zero prior density (out of support).
601            let log_prior = score_log_prior(&model_fn, &proposed);
602            if !log_prior.is_finite() {
603                continue;
604            }
605
606            // (4) Accept within tolerance.
607            let dist = distance_fn.distance(observed_data, &simulator(&proposed));
608            if dist > new_tolerance {
609                continue;
610            }
611
612            // Importance weight: log w = log π(θ) - log Σ_j w_j K(θ | θ_j).
613            let prop_coords = coords_of(&proposed, &coord_addrs);
614            let log_denom =
615                kernel_mixture_log_density(&prop_coords, &prev_coords, &prev_weights, &kernel_std);
616            log_weights.push(log_prior - log_denom);
617            next.push(ABCParticle {
618                trace: proposed,
619                weight: 0.0,
620            });
621        }
622
623        if next.is_empty() || next.len() < n {
624            return Err(ABCError::StageExhausted {
625                tolerance: new_tolerance,
626                accepted: next.len(),
627                requested: n,
628                attempts: max_attempts_per_stage,
629            });
630        }
631
632        // Normalize the importance weights (stable log-sum-exp).
633        let log_norm = log_sum_exp(&log_weights);
634        for (p, &lw) in next.iter_mut().zip(&log_weights) {
635            p.weight = if log_norm.is_finite() {
636                (lw - log_norm).exp()
637            } else {
638                1.0 / n as f64
639            };
640        }
641
642        current = next;
643        current_tolerance = new_tolerance;
644    }
645
646    Ok(ABCSMCResult {
647        particles: current,
648        final_tolerance: current_tolerance,
649    })
650}
651
652/// Sequential Monte Carlo ABC returning an equally-weighted trace population.
653///
654/// This is the correct ABC-SMC of [`abc_smc_weighted`] (fixing finding FG-09),
655/// wrapped for the common case: it runs the weighted algorithm with the default
656/// per-stage attempt budget (`ABC_SMC_DEFAULT_ATTEMPT_FACTOR * particles_per_round`,
657/// finding FG-34) and then resamples the final weighted population down to an
658/// equally-weighted set of traces, so the returned traces can be summarized
659/// directly (e.g. by an unweighted posterior mean).
660///
661/// Unlike the previous implementation, it never panics on an empty initial
662/// population and never loops forever: on any [`ABCError`] it emits a warning and
663/// returns an empty vector. Use [`abc_smc_weighted`] for the weighted population,
664/// a configurable attempt budget, and typed error handling.
665///
666/// # Examples
667///
668/// ```rust
669/// use fugue::{inference::abc::ABCSMCConfig, *};
670/// use rand::rngs::StdRng;
671/// use rand::SeedableRng;
672///
673/// let observed = vec![2.0];
674/// let mut rng = StdRng::seed_from_u64(42);
675///
676/// let samples = abc_smc(
677///     &mut rng,
678///     || sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
679///     |trace| {
680///         if let Some(choice) = trace.choices.get(&addr!("mu")) {
681///             if let ChoiceValue::F64(mu) = choice.value {
682///                 vec![mu]
683///             } else { vec![0.0] }
684///         } else { vec![0.0] }
685///     },
686///     &observed,
687///     &EuclideanDistance,
688///     ABCSMCConfig {
689///         initial_tolerance: 1.0,
690///         tolerance_schedule: vec![0.5],
691///         particles_per_round: 20,
692///     },
693/// );
694/// assert!(!samples.is_empty());
695/// ```
696pub fn abc_smc<A, T, R: Rng>(
697    rng: &mut R,
698    model_fn: impl Fn() -> Model<A>,
699    simulator: impl Fn(&Trace) -> T,
700    observed_data: &T,
701    distance_fn: &dyn DistanceFunction<T>,
702    config: ABCSMCConfig,
703) -> Vec<Trace> {
704    let n = config.particles_per_round;
705    let max_attempts = ABC_SMC_DEFAULT_ATTEMPT_FACTOR.saturating_mul(n.max(1));
706    match abc_smc_weighted(
707        rng,
708        model_fn,
709        simulator,
710        observed_data,
711        distance_fn,
712        config,
713        max_attempts,
714    ) {
715        Ok(result) => {
716            // Resample the weighted population to an equally-weighted trace set,
717            // so the returned traces are a valid unweighted posterior sample.
718            let weights: Vec<f64> = result.particles.iter().map(|p| p.weight).collect();
719            (0..result.particles.len())
720                .map(|_| result.particles[sample_index(rng, &weights)].trace.clone())
721                .collect()
722        }
723        Err(e) => {
724            eprintln!("Warning: ABC-SMC did not complete: {e}. Returning empty population.");
725            Vec::new()
726        }
727    }
728}
729
730/// Ordered list of continuous (f64) sample-site addresses in a trace.
731fn f64_addresses(trace: &Trace) -> Vec<Address> {
732    trace
733        .choices
734        .iter()
735        .filter(|(_, c)| matches!(c.value, ChoiceValue::F64(_)))
736        .map(|(a, _)| a.clone())
737        .collect()
738}
739
740/// Extract the f64 coordinate vector of a trace at the given addresses.
741fn coords_of(trace: &Trace, addrs: &[Address]) -> Vec<f64> {
742    addrs
743        .iter()
744        .map(|a| trace.get_f64(a).unwrap_or(0.0))
745        .collect()
746}
747
748/// Per-component kernel bandwidth `sqrt(2 · weighted variance)` of the population
749/// (Beaumont et al. 2009). Falls back to a small positive value for degenerate
750/// (zero-variance) components so the kernel never collapses to a point mass.
751fn kernel_bandwidths(population: &[ABCParticle], addrs: &[Address]) -> Vec<f64> {
752    let mut std = vec![0.0; addrs.len()];
753    let total_w: f64 = population.iter().map(|p| p.weight).sum();
754    if total_w <= 0.0 {
755        return vec![1e-3; addrs.len()];
756    }
757    for (c, addr) in addrs.iter().enumerate() {
758        let mut mean = 0.0;
759        for p in population {
760            mean += p.weight * p.trace.get_f64(addr).unwrap_or(0.0);
761        }
762        mean /= total_w;
763        let mut var = 0.0;
764        for p in population {
765            let d = p.trace.get_f64(addr).unwrap_or(0.0) - mean;
766            var += p.weight * d * d;
767        }
768        var /= total_w;
769        let bw = (2.0 * var).sqrt();
770        std[c] = if bw > 1e-12 { bw } else { 1e-3 };
771    }
772    std
773}
774
775/// log Σ_j w_j K(x | θ_j) for a component-wise Gaussian kernel with std `kernel_std`.
776fn kernel_mixture_log_density(
777    x: &[f64],
778    centers: &[Vec<f64>],
779    weights: &[f64],
780    kernel_std: &[f64],
781) -> f64 {
782    let terms: Vec<f64> = centers
783        .iter()
784        .zip(weights)
785        .map(|(center, &w)| w.ln() + gaussian_log_density(x, center, kernel_std))
786        .collect();
787    log_sum_exp(&terms)
788}
789
790/// Component-wise Gaussian log density Σ_c log N(x_c; mean_c, std_c).
791fn gaussian_log_density(x: &[f64], mean: &[f64], std: &[f64]) -> f64 {
792    let mut lp = 0.0;
793    for ((&xi, &mi), &si) in x.iter().zip(mean).zip(std) {
794        let s = si.max(1e-12);
795        let z = (xi - mi) / s;
796        lp += -0.5 * z * z - s.ln() - 0.5 * (2.0 * std::f64::consts::PI).ln();
797    }
798    lp
799}
800
801/// Score a trace under the model and return its log prior density log π(θ).
802///
803/// Returns `-inf` when any perturbed value falls outside its support.
804fn score_log_prior<A>(model_fn: &impl Fn() -> Model<A>, trace: &Trace) -> f64 {
805    let (_a, scored) = run(
806        ScoreGivenTrace {
807            base: trace.clone(),
808            trace: Trace::default(),
809        },
810        model_fn(),
811    );
812    scored.log_prior
813}
814
815/// Sample an index in `0..weights.len()` proportional to `weights`.
816fn sample_index<R: Rng>(rng: &mut R, weights: &[f64]) -> usize {
817    let total: f64 = weights.iter().sum();
818    if total <= 0.0 {
819        return rng.gen_range(0..weights.len());
820    }
821    let u = rng.gen::<f64>() * total;
822    let mut cum = 0.0;
823    for (i, &w) in weights.iter().enumerate() {
824        cum += w;
825        if u <= cum {
826            return i;
827        }
828    }
829    weights.len() - 1
830}
831
832/// ABC rejection sampling using scalar summary statistics.
833///
834/// A convenience function for ABC when both observed and simulated data can be
835/// reduced to scalar summary statistics. This is often more efficient than
836/// comparing full datasets and can focus inference on specific aspects of the data.
837///
838/// This function is equivalent to `abc_rejection` but operates on scalar summaries
839/// instead of vector data, making it easier to use for simple cases.
840///
841/// # Arguments
842///
843/// * `rng` - Random number generator
844/// * `model_fn` - Function that creates a model instance
845/// * `simulator` - Function that computes a scalar summary from a trace
846/// * `observed_summary` - Scalar summary of the observed data
847/// * `tolerance` - Maximum allowed absolute difference for acceptance
848/// * `max_samples` - Maximum number of samples to accept
849///
850/// # Returns
851///
852/// Vector of accepted traces that produced summaries within tolerance.
853///
854/// # Examples
855///
856/// ```rust
857/// use fugue::*;
858/// use rand::rngs::StdRng;
859/// use rand::SeedableRng;
860///
861/// // ABC for estimating mean when we only observe sample mean
862/// let observed_mean = 2.0;
863/// let mut rng = StdRng::seed_from_u64(42);
864///
865/// let samples = abc_scalar_summary(
866///     &mut rng,
867///     || sample(addr!("mu"), Normal::new(0.0, 2.0).unwrap()),
868///     |trace| {
869///         // Extract mu parameter and return it as summary
870///         if let Some(choice) = trace.choices.get(&addr!("mu")) {
871///             if let ChoiceValue::F64(mu) = choice.value {
872///                 mu // The summary statistic is just the parameter
873///             } else { 0.0 }
874///         } else { 0.0 }
875///     },
876///     observed_mean,
877///     0.5, // tolerance (larger for easier acceptance)
878///     5,   // max samples (small for test)
879/// );
880/// assert!(!samples.is_empty());
881/// ```
882pub fn abc_scalar_summary<A, R: Rng>(
883    rng: &mut R,
884    model_fn: impl Fn() -> Model<A>,
885    simulator: impl Fn(&Trace) -> f64,
886    observed_summary: f64,
887    tolerance: f64,
888    max_samples: usize,
889) -> Vec<Trace> {
890    abc_rejection(
891        rng,
892        model_fn,
893        |trace| vec![simulator(trace)],
894        &vec![observed_summary],
895        &EuclideanDistance,
896        tolerance,
897        max_samples,
898    )
899}
900
901#[cfg(test)]
902mod tests {
903    use super::*;
904    use crate::addr;
905    use crate::core::distribution::*;
906    use crate::core::model::sample;
907
908    use rand::rngs::StdRng;
909    use rand::SeedableRng;
910
911    #[test]
912    fn distance_functions_work() {
913        let eu = EuclideanDistance;
914        let man = ManhattanDistance;
915        let a = vec![1.0, 2.0, 3.0];
916        let b = vec![1.1, 2.1, 2.9];
917        let d_eu = eu.distance(&a, &b);
918        let d_man = man.distance(&a, &b);
919        assert!(d_eu > 0.0);
920        assert!(d_man > 0.0);
921        // Euclidean should be <= Manhattan for same vectors
922        assert!(d_eu <= d_man + 1e-12);
923    }
924
925    #[test]
926    fn abc_scalar_summary_accepts_with_large_tolerance() {
927        let mut rng = StdRng::seed_from_u64(42);
928        let samples = abc_scalar_summary(
929            &mut rng,
930            || sample(addr!("mu"), Normal::new(0.0, 2.0).unwrap()),
931            |trace| trace.get_f64(&addr!("mu")).unwrap_or(0.0),
932            0.0,  // observed summary
933            10.0, // large tolerance to ensure acceptance
934            3,
935        );
936        assert!(!samples.is_empty());
937    }
938
939    #[test]
940    fn abc_rejection_can_return_empty_with_tight_tolerance() {
941        let mut rng = StdRng::seed_from_u64(43);
942        let observed = vec![1000.0]; // far from prior mean 0
943        let res = abc_rejection(
944            &mut rng,
945            || sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
946            |trace| vec![trace.get_f64(&addr!("mu")).unwrap_or(0.0)],
947            &observed,
948            &EuclideanDistance,
949            1e-6, // extremely tight
950            3,
951        );
952        assert!(res.is_empty());
953    }
954
955    #[test]
956    fn abc_smc_respects_tolerance_schedule() {
957        let mut rng = StdRng::seed_from_u64(44);
958        let observed = vec![0.0];
959        let config = ABCSMCConfig {
960            initial_tolerance: 2.0,
961            tolerance_schedule: vec![1.0, 0.5],
962            particles_per_round: 4,
963        };
964        let res = abc_smc(
965            &mut rng,
966            || sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap()),
967            |trace| vec![trace.get_f64(&addr!("mu")).unwrap_or(0.0)],
968            &observed,
969            &EuclideanDistance,
970            config,
971        );
972        assert_eq!(res.len(), 4);
973    }
974}