use alloc::vec;
use alloc::vec::Vec;
use crate::error::EstimationError;
use crate::linear_algebra::{Cholesky, Matrix, Vector};
use crate::random::{Pcg32, RandomSource};
use crate::scalar::{Numeric, VectorFn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ResamplingScheme {
#[default]
Systematic,
Stratified,
Multinomial,
Residual,
}
impl ResamplingScheme {
pub fn resample_indices<T: Numeric, R: RandomSource>(
self,
weights: &[T],
random: &mut R,
indices: &mut [usize],
) {
match self {
ResamplingScheme::Systematic => systematic(weights, random, indices),
ResamplingScheme::Stratified => stratified(weights, random, indices),
ResamplingScheme::Multinomial => multinomial(weights, random, indices),
ResamplingScheme::Residual => residual(weights, random, indices),
}
}
}
fn systematic<T: Numeric, R: RandomSource>(weights: &[T], random: &mut R, indices: &mut [usize]) {
let count = indices.len();
let count_scalar = T::from_usize(count);
let offset = T::from_f64(random.next_unit_f64());
let mut running_sum = weights[0];
let mut source = 0;
for (step, slot) in indices.iter_mut().enumerate() {
let position = (offset + T::from_usize(step)) / count_scalar;
while position >= running_sum && source + 1 < count {
source += 1;
running_sum += weights[source];
}
*slot = source;
}
}
fn stratified<T: Numeric, R: RandomSource>(weights: &[T], random: &mut R, indices: &mut [usize]) {
let count = indices.len();
let count_scalar = T::from_usize(count);
let mut running_sum = weights[0];
let mut source = 0;
for (step, slot) in indices.iter_mut().enumerate() {
let offset = T::from_f64(random.next_unit_f64());
let position = (offset + T::from_usize(step)) / count_scalar;
while position >= running_sum && source + 1 < count {
source += 1;
running_sum += weights[source];
}
*slot = source;
}
}
fn multinomial<T: Numeric, R: RandomSource>(weights: &[T], random: &mut R, indices: &mut [usize]) {
for slot in indices.iter_mut() {
let draw = T::from_f64(random.next_unit_f64());
*slot = draw_from_weights(weights, draw);
}
}
fn residual<T: Numeric, R: RandomSource>(weights: &[T], random: &mut R, indices: &mut [usize]) {
let count = indices.len();
let count_scalar = T::from_usize(count);
let mut filled = 0;
for (source, &weight) in weights.iter().enumerate() {
let target = (count_scalar * weight).floor();
let mut laid = T::ZERO;
while laid < target && filled < count {
indices[filled] = source;
filled += 1;
laid += T::ONE;
}
}
let mut remainder_sum = T::ZERO;
for &weight in weights {
let scaled = count_scalar * weight;
remainder_sum += scaled - scaled.floor();
}
for slot in indices.iter_mut().skip(filled) {
let draw = T::from_f64(random.next_unit_f64());
*slot = draw_from_remainders(weights, count_scalar, remainder_sum, draw);
}
}
fn draw_from_weights<T: Numeric>(weights: &[T], draw: T) -> usize {
let mut running_sum = T::ZERO;
for (source, &weight) in weights.iter().enumerate() {
running_sum += weight;
if running_sum >= draw {
return source;
}
}
weights.len() - 1
}
fn draw_from_remainders<T: Numeric>(
weights: &[T],
count_scalar: T,
remainder_sum: T,
draw: T,
) -> usize {
let mut running_sum = T::ZERO;
for (source, &weight) in weights.iter().enumerate() {
let scaled = count_scalar * weight;
running_sum += (scaled - scaled.floor()) / remainder_sum;
if running_sum >= draw {
return source;
}
}
weights.len() - 1
}
pub trait Likelihood<const MEASUREMENT_DIMENSION: usize, T: Numeric> {
fn log_weight(
&self,
predicted: &[T; MEASUREMENT_DIMENSION],
measurement: &[T; MEASUREMENT_DIMENSION],
) -> T;
}
#[derive(Debug, Clone, Copy)]
pub struct GaussianLikelihood<const MEASUREMENT_DIMENSION: usize, T = f64> {
noise_factor: Cholesky<MEASUREMENT_DIMENSION, T>,
}
impl<const MEASUREMENT_DIMENSION: usize, T: Numeric> GaussianLikelihood<MEASUREMENT_DIMENSION, T> {
pub fn new(
measurement_noise: Matrix<MEASUREMENT_DIMENSION, MEASUREMENT_DIMENSION, T>,
) -> Result<Self, EstimationError> {
let noise_factor = measurement_noise
.cholesky()
.map_err(|_| EstimationError::NotPositiveDefinite)?;
Ok(GaussianLikelihood { noise_factor })
}
}
impl<const MEASUREMENT_DIMENSION: usize, T: Numeric> Likelihood<MEASUREMENT_DIMENSION, T>
for GaussianLikelihood<MEASUREMENT_DIMENSION, T>
{
fn log_weight(
&self,
predicted: &[T; MEASUREMENT_DIMENSION],
measurement: &[T; MEASUREMENT_DIMENSION],
) -> T {
let residual = Vector::from_fn(|i| measurement[i] - predicted[i]);
let solved = self.noise_factor.solve(residual);
-T::HALF * residual.dot(solved)
}
}
#[derive(Debug, Clone)]
pub struct ParticleFilter<
const STATE_DIMENSION: usize,
const MEASUREMENT_DIMENSION: usize,
T = f64,
R = Pcg32,
> {
particles: Vec<Vector<STATE_DIMENSION, T>>,
weights: Vec<T>,
process_noise_factor: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
resampling: ResamplingScheme,
resample_threshold: T,
roughening: T,
random: R,
particle_scratch: Vec<Vector<STATE_DIMENSION, T>>,
index_scratch: Vec<usize>,
log_weight_scratch: Vec<T>,
}
impl<const STATE_DIMENSION: usize, const MEASUREMENT_DIMENSION: usize, T: Numeric>
ParticleFilter<STATE_DIMENSION, MEASUREMENT_DIMENSION, T, Pcg32>
{
pub fn new(
particle_count: usize,
initial_mean: Vector<STATE_DIMENSION, T>,
initial_covariance: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
process_noise: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
seed: u64,
) -> Result<Self, EstimationError> {
Self::from_random(
particle_count,
initial_mean,
initial_covariance,
process_noise,
Pcg32::new(seed),
)
}
}
impl<const STATE_DIMENSION: usize, const MEASUREMENT_DIMENSION: usize, T: Numeric, R>
ParticleFilter<STATE_DIMENSION, MEASUREMENT_DIMENSION, T, R>
where
R: RandomSource,
{
pub fn from_random(
particle_count: usize,
initial_mean: Vector<STATE_DIMENSION, T>,
initial_covariance: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
process_noise: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
mut random: R,
) -> Result<Self, EstimationError> {
const {
assert!(
STATE_DIMENSION > 0,
"ParticleFilter: STATE_DIMENSION must be non-zero"
)
};
const {
assert!(
MEASUREMENT_DIMENSION > 0,
"ParticleFilter: MEASUREMENT_DIMENSION must be non-zero"
)
};
if particle_count == 0 {
return Err(EstimationError::WeightsDegenerate);
}
let initial_factor = initial_covariance
.cholesky()
.map_err(|_| EstimationError::NotPositiveDefinite)?
.l();
let process_noise_factor = process_noise
.cholesky()
.map_err(|_| EstimationError::NotPositiveDefinite)?
.l();
let mut particles = Vec::with_capacity(particle_count);
for _ in 0..particle_count {
let sample = Vector::from_fn(|_| T::from_f64(random.standard_normal()));
particles.push(initial_mean + initial_factor * sample);
}
let weights = vec![T::ONE / T::from_usize(particle_count); particle_count];
let particle_scratch = particles.clone();
let index_scratch = vec![0usize; particle_count];
let log_weight_scratch = vec![T::ZERO; particle_count];
Ok(ParticleFilter {
particles,
weights,
process_noise_factor,
resampling: ResamplingScheme::Systematic,
resample_threshold: T::from_usize(particle_count) * T::HALF,
roughening: T::ZERO,
random,
particle_scratch,
index_scratch,
log_weight_scratch,
})
}
#[must_use]
pub const fn with_resampling(mut self, scheme: ResamplingScheme) -> Self {
self.resampling = scheme;
self
}
#[must_use]
pub fn with_resample_threshold(mut self, threshold: T) -> Self {
self.resample_threshold = threshold;
self
}
#[must_use]
pub fn with_roughening(mut self, scale: T) -> Self {
self.roughening = scale;
self
}
pub fn set_process_noise(
&mut self,
process_noise: Matrix<STATE_DIMENSION, STATE_DIMENSION, T>,
) -> Result<(), EstimationError> {
self.process_noise_factor = process_noise
.cholesky()
.map_err(|_| EstimationError::NotPositiveDefinite)?
.l();
Ok(())
}
pub fn predict<ProcessModel>(
&mut self,
process_model: &ProcessModel,
) -> Result<(), EstimationError>
where
ProcessModel: VectorFn<STATE_DIMENSION, STATE_DIMENSION>,
{
let mut finite = true;
for particle in self.particles.iter_mut() {
let propagated = Vector::new(process_model.eval(particle.as_array()));
let noise = Vector::from_fn(|_| T::from_f64(self.random.standard_normal()));
*particle = propagated + self.process_noise_factor * noise;
if !particle.is_finite() {
finite = false;
}
}
if finite {
Ok(())
} else {
Err(EstimationError::NonFinite)
}
}
pub fn update<MeasurementModel, L>(
&mut self,
measurement_model: &MeasurementModel,
likelihood: &L,
measurement: Vector<MEASUREMENT_DIMENSION, T>,
) -> Result<(), EstimationError>
where
MeasurementModel: VectorFn<STATE_DIMENSION, MEASUREMENT_DIMENSION>,
L: Likelihood<MEASUREMENT_DIMENSION, T>,
{
if !measurement.is_finite() {
return Err(EstimationError::NonFinite);
}
for i in 0..self.particles.len() {
let predicted = measurement_model.eval(self.particles[i].as_array());
let score = likelihood.log_weight(&predicted, measurement.as_array());
self.log_weight_scratch[i] = self.weights[i].ln() + score;
}
self.normalize_and_resample()
}
pub fn update_with_log_weights<F>(&mut self, mut score: F) -> Result<(), EstimationError>
where
F: FnMut(&Vector<STATE_DIMENSION, T>) -> T,
{
for i in 0..self.particles.len() {
self.log_weight_scratch[i] = self.weights[i].ln() + score(&self.particles[i]);
}
self.normalize_and_resample()
}
fn normalize_and_resample(&mut self) -> Result<(), EstimationError> {
let mut largest = T::NEG_INFINITY;
for &log_weight in &self.log_weight_scratch {
if log_weight > largest {
largest = log_weight;
}
}
if !largest.is_finite() {
return Err(EstimationError::WeightsDegenerate);
}
let mut sum = T::ZERO;
for i in 0..self.weights.len() {
let weight = (self.log_weight_scratch[i] - largest).exp();
self.weights[i] = weight;
sum += weight;
}
if sum <= T::ZERO || !sum.is_finite() {
return Err(EstimationError::WeightsDegenerate);
}
for weight in self.weights.iter_mut() {
*weight /= sum;
}
if self.effective_sample_size() < self.resample_threshold {
self.resample();
}
Ok(())
}
pub fn resample(&mut self) {
self.resampling
.resample_indices(&self.weights, &mut self.random, &mut self.index_scratch);
for (destination, &source) in self.index_scratch.iter().enumerate() {
self.particle_scratch[destination] = self.particles[source];
}
core::mem::swap(&mut self.particles, &mut self.particle_scratch);
let uniform = T::ONE / T::from_usize(self.weights.len());
for weight in self.weights.iter_mut() {
*weight = uniform;
}
if self.roughening > T::ZERO {
self.roughen();
}
}
fn roughen(&mut self) {
for axis in 0..STATE_DIMENSION {
let mut lowest = self.particles[0][axis];
let mut highest = lowest;
for particle in &self.particles {
let value = particle[axis];
if value < lowest {
lowest = value;
}
if value > highest {
highest = value;
}
}
let scale = self.roughening * (highest - lowest);
if scale <= T::ZERO {
continue;
}
for particle in self.particles.iter_mut() {
particle[axis] += scale * T::from_f64(self.random.standard_normal());
}
}
}
pub fn particles(&self) -> &[Vector<STATE_DIMENSION, T>] {
&self.particles
}
#[must_use]
pub fn weights(&self) -> &[T] {
&self.weights
}
#[must_use]
pub fn effective_sample_size(&self) -> T {
let mut sum_of_squares = T::ZERO;
for &weight in &self.weights {
sum_of_squares += weight * weight;
}
T::ONE / sum_of_squares
}
pub fn mean(&self) -> Vector<STATE_DIMENSION, T> {
let mut accumulated = Vector::zeros();
for (particle, &weight) in self.particles.iter().zip(&self.weights) {
accumulated += particle.scale(weight);
}
accumulated
}
pub fn maximum_a_posteriori_state(&self) -> Vector<STATE_DIMENSION, T> {
let mut best = 0;
let mut best_weight = self.weights[0];
for (index, &weight) in self.weights.iter().enumerate() {
if weight > best_weight {
best_weight = weight;
best = index;
}
}
self.particles[best]
}
}