use crate::error::{StatsError, StatsResult as Result};
use scirs2_core::ndarray::{Array1, Array2, Axis};
use scirs2_core::validation::*;
use scirs2_core::Rng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResamplingStrategy {
Systematic,
Multinomial,
Stratified,
Residual,
}
impl Default for ResamplingStrategy {
fn default() -> Self {
ResamplingStrategy::Systematic
}
}
pub fn normalize_log_weights(log_weights: &Array1<f64>) -> (Array1<f64>, f64) {
let max_lw = log_weights
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
if max_lw.is_infinite() && max_lw < 0.0 {
let n = log_weights.len();
let uniform = Array1::from_elem(n, 1.0 / n as f64);
return (uniform, f64::NEG_INFINITY);
}
let shifted: Array1<f64> = log_weights.mapv(|lw| (lw - max_lw).exp());
let sum_shifted = shifted.sum();
let log_evidence = max_lw + sum_shifted.ln();
let weights = shifted / sum_shifted;
(weights, log_evidence)
}
pub fn effective_sample_size(weights: &Array1<f64>) -> f64 {
let sum_sq: f64 = weights.iter().map(|&w| w * w).sum();
if sum_sq <= 0.0 {
0.0
} else {
1.0 / sum_sq
}
}
pub fn resample<R: Rng + ?Sized>(
weights: &Array1<f64>,
strategy: ResamplingStrategy,
rng: &mut R,
) -> Result<Vec<usize>> {
let n = weights.len();
if n == 0 {
return Err(StatsError::InvalidArgument(
"Weights array must be non-empty".to_string(),
));
}
let weight_sum: f64 = weights.iter().sum();
if (weight_sum - 1.0).abs() > 1e-6 {
return Err(StatsError::ComputationError(format!(
"Weights must sum to 1, got {}",
weight_sum
)));
}
match strategy {
ResamplingStrategy::Systematic => systematic_resample(weights, n, rng),
ResamplingStrategy::Multinomial => multinomial_resample(weights, n, rng),
ResamplingStrategy::Stratified => stratified_resample(weights, n, rng),
ResamplingStrategy::Residual => residual_resample(weights, n, rng),
}
}
fn systematic_resample<R: Rng + ?Sized>(
weights: &Array1<f64>,
n: usize,
rng: &mut R,
) -> Result<Vec<usize>> {
let u0: f64 = rng.random::<f64>() / n as f64;
let mut indices = Vec::with_capacity(n);
let mut cumsum = 0.0_f64;
let mut j = 0_usize;
for i in 0..n {
let threshold = u0 + i as f64 / n as f64;
while cumsum < threshold && j < n {
cumsum += weights[j];
if cumsum >= threshold {
break;
}
j += 1;
}
indices.push(j.min(n - 1));
}
Ok(indices)
}
fn multinomial_resample<R: Rng + ?Sized>(
weights: &Array1<f64>,
n: usize,
rng: &mut R,
) -> Result<Vec<usize>> {
let mut cdf = Vec::with_capacity(n);
let mut cumsum = 0.0_f64;
for &w in weights.iter() {
cumsum += w;
cdf.push(cumsum);
}
let mut indices = Vec::with_capacity(n);
for _ in 0..n {
let u: f64 = rng.random();
let idx = cdf.partition_point(|&c| c < u);
indices.push(idx.min(n - 1));
}
Ok(indices)
}
fn stratified_resample<R: Rng + ?Sized>(
weights: &Array1<f64>,
n: usize,
rng: &mut R,
) -> Result<Vec<usize>> {
let mut cdf = Vec::with_capacity(n);
let mut cumsum = 0.0_f64;
for &w in weights.iter() {
cumsum += w;
cdf.push(cumsum);
}
let mut indices = Vec::with_capacity(n);
for i in 0..n {
let u: f64 = (i as f64 + rng.random::<f64>()) / n as f64;
let idx = cdf.partition_point(|&c| c < u);
indices.push(idx.min(n - 1));
}
Ok(indices)
}
fn residual_resample<R: Rng + ?Sized>(
weights: &Array1<f64>,
n: usize,
rng: &mut R,
) -> Result<Vec<usize>> {
let mut indices = Vec::with_capacity(n);
let mut residuals = Vec::with_capacity(n);
let mut n_residual = n;
for (i, &w) in weights.iter().enumerate() {
let floor_count = (w * n as f64).floor() as usize;
for _ in 0..floor_count {
indices.push(i);
}
let residual = w * n as f64 - floor_count as f64;
residuals.push(residual);
n_residual = n_residual.saturating_sub(floor_count);
}
let residual_sum: f64 = residuals.iter().sum();
if n_residual > 0 && residual_sum > 0.0 {
let residual_weights: Array1<f64> =
Array1::from_vec(residuals.iter().map(|&r| r / residual_sum).collect());
let residual_indices = multinomial_resample(&residual_weights, n_residual, rng)?;
indices.extend_from_slice(&residual_indices);
}
Ok(indices)
}
pub trait ObservationModel: Send + Sync {
fn log_likelihood(&self, state: &Array1<f64>, observation: &Array1<f64>) -> f64;
}
pub trait TransitionKernel: Send + Sync {
fn sample<R: Rng + ?Sized>(&self, state: &Array1<f64>, rng: &mut R) -> Array1<f64>;
}
pub trait PriorDistribution: Send + Sync {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Array1<f64>;
fn log_density(&self, x: &Array1<f64>) -> f64;
}
pub trait SmcTargetDistribution: Send + Sync {
fn log_density(&self, x: &Array1<f64>, beta: f64) -> f64;
fn dim(&self) -> usize;
}
pub trait SmcKernel: Send + Sync {
fn step<R: Rng + ?Sized>(
&self,
state: &Array1<f64>,
beta: f64,
target: &dyn SmcTargetDistribution,
rng: &mut R,
) -> Array1<f64>;
}
pub struct ParticleFilter<O, T, P>
where
O: ObservationModel,
T: TransitionKernel,
P: PriorDistribution,
{
pub observation_model: O,
pub transition_kernel: T,
pub prior: P,
pub particles: Array2<f64>,
pub log_weights: Array1<f64>,
pub log_marginal_likelihood: f64,
pub n_particles: usize,
pub state_dim: usize,
pub ess_threshold: f64,
pub resampling_strategy: ResamplingStrategy,
pub t: usize,
}
impl<O, T, P> ParticleFilter<O, T, P>
where
O: ObservationModel,
T: TransitionKernel,
P: PriorDistribution,
{
pub fn new<R: Rng + ?Sized>(
n_particles: usize,
state_dim: usize,
observation_model: O,
transition_kernel: T,
prior: P,
ess_threshold: f64,
strategy: ResamplingStrategy,
rng: &mut R,
) -> Result<Self> {
check_positive(n_particles, "n_particles")?;
check_positive(state_dim, "state_dim")?;
if !(0.0..=1.0).contains(&ess_threshold) {
return Err(StatsError::InvalidArgument(
"ess_threshold must be in [0, 1]".to_string(),
));
}
let mut particles = Array2::zeros((n_particles, state_dim));
for i in 0..n_particles {
let sample = prior.sample(rng);
if sample.len() != state_dim {
return Err(StatsError::DimensionMismatch(format!(
"Prior sample has dimension {} but state_dim is {}",
sample.len(),
state_dim
)));
}
particles.row_mut(i).assign(&sample);
}
let log_w0 = -(n_particles as f64).ln();
let log_weights = Array1::from_elem(n_particles, log_w0);
Ok(Self {
observation_model,
transition_kernel,
prior,
particles,
log_weights,
log_marginal_likelihood: 0.0,
n_particles,
state_dim,
ess_threshold,
resampling_strategy: strategy,
t: 0,
})
}
pub fn update<R: Rng + ?Sized>(
&mut self,
observation: &Array1<f64>,
rng: &mut R,
) -> Result<f64> {
let mut new_particles = Array2::zeros((self.n_particles, self.state_dim));
for i in 0..self.n_particles {
let x_prev = self.particles.row(i).to_owned();
let x_new = self.transition_kernel.sample(&x_prev, rng);
new_particles.row_mut(i).assign(&x_new);
}
self.particles = new_particles;
let mut new_log_weights = Array1::zeros(self.n_particles);
for i in 0..self.n_particles {
let x = self.particles.row(i).to_owned();
let log_lik = self.observation_model.log_likelihood(&x, observation);
new_log_weights[i] = self.log_weights[i] + log_lik;
}
let (normalised_weights, log_evidence_increment) =
normalize_log_weights(&new_log_weights);
self.log_marginal_likelihood += log_evidence_increment;
self.log_weights = new_log_weights;
let ess = effective_sample_size(&normalised_weights);
if ess < self.ess_threshold * self.n_particles as f64 {
let indices =
resample(&normalised_weights, self.resampling_strategy, rng)?;
let old_particles = self.particles.clone();
for (i, &parent) in indices.iter().enumerate() {
let row = old_particles.row(parent).to_owned();
self.particles.row_mut(i).assign(&row);
}
let log_w_uniform = -(self.n_particles as f64).ln();
self.log_weights = Array1::from_elem(self.n_particles, log_w_uniform);
}
self.t += 1;
Ok(self.log_marginal_likelihood)
}
pub fn filter<R: Rng + ?Sized>(
&mut self,
observations: &Array2<f64>,
rng: &mut R,
) -> Result<(Array2<f64>, f64)> {
let n_steps = observations.nrows();
let mut filtered_means = Array2::zeros((n_steps, self.state_dim));
for t in 0..n_steps {
let obs = observations.row(t).to_owned();
self.update(&obs, rng)?;
let (norm_w, _) = normalize_log_weights(&self.log_weights);
let mut mean = Array1::zeros(self.state_dim);
for i in 0..self.n_particles {
let row = self.particles.row(i);
mean = mean + row.to_owned() * norm_w[i];
}
filtered_means.row_mut(t).assign(&mean);
}
Ok((filtered_means, self.log_marginal_likelihood))
}
pub fn current_approximation(&self) -> (Array2<f64>, Array1<f64>) {
let (weights, _) = normalize_log_weights(&self.log_weights);
(self.particles.clone(), weights)
}
}
pub struct RandomWalkKernel {
pub step_size: f64,
}
impl RandomWalkKernel {
pub fn new(step_size: f64) -> Result<Self> {
if step_size <= 0.0 {
return Err(StatsError::InvalidArgument(
"step_size must be positive".to_string(),
));
}
Ok(Self { step_size })
}
}
impl SmcKernel for RandomWalkKernel {
fn step<R: Rng + ?Sized>(
&self,
state: &Array1<f64>,
beta: f64,
target: &dyn SmcTargetDistribution,
rng: &mut R,
) -> Array1<f64> {
let dim = state.len();
let mut proposal = state.clone();
for i in 0..dim {
let u1: f64 = rng.random::<f64>().max(f64::EPSILON);
let u2: f64 = rng.random::<f64>();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
proposal[i] = state[i] + self.step_size * z;
}
let log_accept = target.log_density(&proposal, beta)
- target.log_density(state, beta);
let u: f64 = rng.random();
if u.ln() < log_accept {
proposal
} else {
state.clone()
}
}
}
pub struct SmcSampler<T, K>
where
T: SmcTargetDistribution,
K: SmcKernel,
{
pub target: T,
pub kernel: K,
pub particles: Array2<f64>,
pub log_weights: Array1<f64>,
pub log_evidence: f64,
pub betas: Vec<f64>,
pub n_mcmc_steps: usize,
pub resampling_strategy: ResamplingStrategy,
pub ess_threshold: f64,
pub n_particles: usize,
}
impl<T: SmcTargetDistribution, K: SmcKernel> SmcSampler<T, K> {
pub fn new(
n_particles: usize,
target: T,
kernel: K,
betas: Option<Vec<f64>>,
n_mcmc_steps: usize,
ess_threshold: f64,
strategy: ResamplingStrategy,
) -> Result<Self> {
check_positive(n_particles, "n_particles")?;
let betas = match betas {
Some(b) => b,
None => {
let n_steps = 10_usize;
(0..=n_steps)
.map(|i| (i as f64 / n_steps as f64).powi(2))
.collect()
}
};
if betas.is_empty() || (betas[0] - 0.0).abs() > 1e-12 {
return Err(StatsError::InvalidArgument(
"betas must be non-empty and start at 0".to_string(),
));
}
if (betas[betas.len() - 1] - 1.0).abs() > 1e-12 {
return Err(StatsError::InvalidArgument(
"betas must end at 1".to_string(),
));
}
for w in betas.windows(2) {
if w[1] <= w[0] {
return Err(StatsError::InvalidArgument(
"betas must be strictly increasing".to_string(),
));
}
}
if !(0.0..=1.0).contains(&ess_threshold) {
return Err(StatsError::InvalidArgument(
"ess_threshold must be in [0, 1]".to_string(),
));
}
let dim = target.dim();
let particles = Array2::zeros((n_particles, dim));
let log_w0 = -(n_particles as f64).ln();
let log_weights = Array1::from_elem(n_particles, log_w0);
Ok(Self {
target,
kernel,
particles,
log_weights,
log_evidence: 0.0,
betas,
n_mcmc_steps,
resampling_strategy: strategy,
ess_threshold,
n_particles,
})
}
pub fn initialise<R: Rng + ?Sized, F>(&mut self, prior_sampler: F, rng: &mut R) -> Result<()>
where
F: Fn(&mut R) -> Array1<f64>,
{
let dim = self.target.dim();
for i in 0..self.n_particles {
let sample = prior_sampler(rng);
if sample.len() != dim {
return Err(StatsError::DimensionMismatch(format!(
"Prior sample has dimension {} but target.dim() is {}",
sample.len(),
dim
)));
}
self.particles.row_mut(i).assign(&sample);
}
let log_w0 = -(self.n_particles as f64).ln();
self.log_weights = Array1::from_elem(self.n_particles, log_w0);
self.log_evidence = 0.0;
Ok(())
}
pub fn run<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<(Array2<f64>, f64)> {
let n_betas = self.betas.len();
for level in 1..n_betas {
let beta_prev = self.betas[level - 1];
let beta_curr = self.betas[level];
let mut new_log_weights = Array1::zeros(self.n_particles);
for i in 0..self.n_particles {
let x = self.particles.row(i).to_owned();
let log_w_increment = self.target.log_density(&x, beta_curr)
- self.target.log_density(&x, beta_prev);
new_log_weights[i] = self.log_weights[i] + log_w_increment;
}
let (normalised_weights, log_z_increment) =
normalize_log_weights(&new_log_weights);
self.log_evidence += log_z_increment;
self.log_weights = new_log_weights;
let ess = effective_sample_size(&normalised_weights);
if ess < self.ess_threshold * self.n_particles as f64 {
let indices =
resample(&normalised_weights, self.resampling_strategy, rng)?;
let old_particles = self.particles.clone();
for (i, &parent) in indices.iter().enumerate() {
let row = old_particles.row(parent).to_owned();
self.particles.row_mut(i).assign(&row);
}
let log_w_uniform = -(self.n_particles as f64).ln();
self.log_weights = Array1::from_elem(self.n_particles, log_w_uniform);
}
for i in 0..self.n_particles {
let mut state = self.particles.row(i).to_owned();
for _ in 0..self.n_mcmc_steps {
state =
self.kernel
.step(&state, beta_curr, &self.target, rng);
}
self.particles.row_mut(i).assign(&state);
}
}
Ok((self.particles.clone(), self.log_evidence))
}
pub fn current_approximation(&self) -> (Array2<f64>, Array1<f64>) {
let (weights, _) = normalize_log_weights(&self.log_weights);
(self.particles.clone(), weights)
}
pub fn weighted_mean(&self) -> Array1<f64> {
let (weights, _) = normalize_log_weights(&self.log_weights);
let dim = self.target.dim();
let mut mean = Array1::zeros(dim);
for i in 0..self.n_particles {
let row = self.particles.row(i);
mean = mean + row.to_owned() * weights[i];
}
mean
}
pub fn weighted_variance(&self) -> Array1<f64> {
let (weights, _) = normalize_log_weights(&self.log_weights);
let mean = self.weighted_mean();
let dim = self.target.dim();
let mut variance = Array1::zeros(dim);
for i in 0..self.n_particles {
let row = self.particles.row(i);
let diff = row.to_owned() - &mean;
variance = variance + diff.mapv(|v| v * v) * weights[i];
}
variance
}
}
#[derive(Debug, Clone)]
pub struct GaussianPrior {
pub mean: Array1<f64>,
pub std: f64,
}
impl GaussianPrior {
pub fn new(mean: Array1<f64>, std: f64) -> Result<Self> {
if std <= 0.0 {
return Err(StatsError::InvalidArgument(
"std must be positive".to_string(),
));
}
Ok(Self { mean, std })
}
}
impl PriorDistribution for GaussianPrior {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Array1<f64> {
let dim = self.mean.len();
let mut out = self.mean.clone();
for i in 0..dim {
let u1: f64 = rng.random::<f64>().max(f64::EPSILON);
let u2: f64 = rng.random::<f64>();
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
out[i] = self.mean[i] + self.std * z;
}
out
}
fn log_density(&self, x: &Array1<f64>) -> f64 {
let dim = self.mean.len() as f64;
let inv_var = 1.0 / (self.std * self.std);
let diff = x - &self.mean;
let quad = diff.iter().map(|&v| v * v).sum::<f64>() * inv_var;
-0.5 * quad - 0.5 * dim * (2.0 * std::f64::consts::PI * self.std * self.std).ln()
}
}
pub struct GaussianSmcTarget {
pub prior_mean: Array1<f64>,
pub prior_var: f64,
pub likelihood_mean: Array1<f64>,
pub likelihood_var: f64,
}
impl GaussianSmcTarget {
pub fn new(
prior_mean: Array1<f64>,
prior_var: f64,
likelihood_mean: Array1<f64>,
likelihood_var: f64,
) -> Result<Self> {
if prior_mean.len() != likelihood_mean.len() {
return Err(StatsError::DimensionMismatch(
"prior_mean and likelihood_mean must have the same length".to_string(),
));
}
if prior_var <= 0.0 || likelihood_var <= 0.0 {
return Err(StatsError::InvalidArgument(
"Variances must be positive".to_string(),
));
}
Ok(Self {
prior_mean,
prior_var,
likelihood_mean,
likelihood_var,
})
}
}
impl SmcTargetDistribution for GaussianSmcTarget {
fn log_density(&self, x: &Array1<f64>, beta: f64) -> f64 {
let dim = self.prior_mean.len() as f64;
let prior_diff = x - &self.prior_mean;
let log_prior = -0.5
* prior_diff.iter().map(|&v| v * v).sum::<f64>()
/ self.prior_var
- 0.5 * dim * (2.0 * std::f64::consts::PI * self.prior_var).ln();
let lik_diff = x - &self.likelihood_mean;
let log_lik = -0.5
* lik_diff.iter().map(|&v| v * v).sum::<f64>()
/ self.likelihood_var
- 0.5 * dim * (2.0 * std::f64::consts::PI * self.likelihood_var).ln();
(1.0 - beta) * log_prior + beta * log_lik
}
fn dim(&self) -> usize {
self.prior_mean.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
use scirs2_core::random::SmallRng;
use scirs2_core::random::SeedableRng;
#[test]
fn test_normalize_log_weights() {
let log_w = array![0.0_f64, 0.0, 0.0, 0.0];
let (w, log_z) = normalize_log_weights(&log_w);
assert!((w.sum() - 1.0).abs() < 1e-12);
assert!((log_z - (4.0_f64).ln()).abs() < 1e-10);
}
#[test]
fn test_effective_sample_size() {
let n = 100_usize;
let weights = Array1::from_elem(n, 1.0 / n as f64);
let ess = effective_sample_size(&weights);
assert!((ess - n as f64).abs() < 1e-10);
let mut degenerate = Array1::zeros(n);
degenerate[0] = 1.0;
let ess_deg = effective_sample_size(°enerate);
assert!((ess_deg - 1.0).abs() < 1e-10);
}
#[test]
fn test_systematic_resample() {
let mut rng = SmallRng::seed_from_u64(42);
let weights = array![0.5_f64, 0.3, 0.1, 0.1];
let indices = resample(&weights, ResamplingStrategy::Systematic, &mut rng)
.expect("resample should succeed");
assert_eq!(indices.len(), 4);
assert!(indices.iter().all(|&i| i < 4));
}
#[test]
fn test_stratified_resample() {
let mut rng = SmallRng::seed_from_u64(99);
let weights = array![0.25_f64, 0.25, 0.25, 0.25];
let indices = resample(&weights, ResamplingStrategy::Stratified, &mut rng)
.expect("resample should succeed");
assert_eq!(indices.len(), 4);
}
#[test]
fn test_multinomial_resample() {
let mut rng = SmallRng::seed_from_u64(7);
let weights = array![0.5_f64, 0.25, 0.25];
let indices = resample(&weights, ResamplingStrategy::Multinomial, &mut rng)
.expect("resample should succeed");
assert_eq!(indices.len(), 3);
}
#[test]
fn test_smc_sampler_gaussian() {
let mut rng = SmallRng::seed_from_u64(12345);
let dim = 2_usize;
let prior_mean = Array1::zeros(dim);
let likelihood_mean = Array1::from_elem(dim, 2.0);
let target = GaussianSmcTarget::new(
prior_mean.clone(),
1.0,
likelihood_mean.clone(),
0.5,
)
.expect("target creation should succeed");
let kernel = RandomWalkKernel::new(0.3).expect("kernel creation should succeed");
let betas: Vec<f64> = (0..=5).map(|i| i as f64 / 5.0).collect();
let mut sampler = SmcSampler::new(
200,
target,
kernel,
Some(betas),
3,
0.5,
ResamplingStrategy::Systematic,
)
.expect("sampler creation should succeed");
let prior = GaussianPrior::new(prior_mean, 1.0).expect("prior creation should succeed");
sampler
.initialise(|rng| prior.sample(rng), &mut rng)
.expect("initialise should succeed");
let (_samples, log_z) = sampler.run(&mut rng).expect("run should succeed");
assert!(log_z.is_finite());
let mean = sampler.weighted_mean();
for &m in mean.iter() {
assert!(m > -1.0 && m < 3.5, "mean {} out of expected range", m);
}
}
#[test]
fn test_gaussian_prior_density() {
let prior = GaussianPrior::new(Array1::zeros(2), 1.0).expect("should succeed");
let x = Array1::zeros(2);
let log_p = prior.log_density(&x);
assert!((log_p - (-2.0 * std::f64::consts::PI).ln() + 0.0).abs() < 0.1);
}
}