use crate::error::EstimationError;
use crate::estimation::ParticleFilter;
use crate::linear_algebra::{Matrix, Vector};
use crate::mapping::{OccupancyMap, ScanGeometry};
use crate::random::RandomScalar;
use crate::scalar::{Numeric, Primal, VectorFn};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InitialParticleCloud<T: Numeric = f64> {
pub particle_count: usize,
pub position_variance: T,
pub heading_variance: T,
}
impl<T: Numeric> Default for InitialParticleCloud<T> {
fn default() -> Self {
InitialParticleCloud {
particle_count: 2000,
position_variance: T::from_f64(0.16),
heading_variance: T::from_f64(4.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BeamModel<T: Numeric = f64> {
pub range_deviation: T,
pub agreement_reward: T,
pub mismatch_penalty: T,
}
impl<T: Numeric> Default for BeamModel<T> {
fn default() -> Self {
BeamModel {
range_deviation: T::from_f64(0.10),
agreement_reward: T::from_f64(0.1),
mismatch_penalty: T::from_f64(-5.0),
}
}
}
#[derive(Debug, Clone)]
pub struct MonteCarloLocalizer<const NUM_BEAMS: usize, T: RandomScalar + Primal = f64> {
filter: ParticleFilter<3, 1, T>,
beam_model: BeamModel<T>,
}
impl<const NUM_BEAMS: usize, T: RandomScalar + Primal> MonteCarloLocalizer<NUM_BEAMS, T> {
pub fn new(
hint: [T; 3],
cloud: InitialParticleCloud<T>,
beam_model: BeamModel<T>,
seed: u64,
) -> Result<Self, EstimationError> {
let spread = Matrix::from_diagonal([
cloud.position_variance,
cloud.position_variance,
cloud.heading_variance,
]);
let filter = ParticleFilter::<3, 1, T>::new(
cloud.particle_count,
Vector::new(hint),
spread,
Matrix::from_diagonal([T::from_f64(1e-4); 3]),
seed,
)?;
Ok(MonteCarloLocalizer { filter, beam_model })
}
pub fn set_motion_noise(&mut self, variances: [T; 3]) -> Result<(), EstimationError> {
self.filter
.set_process_noise(Matrix::from_diagonal(variances))
}
pub fn predict(
&mut self,
delta_arc_length: T,
delta_heading: T,
) -> Result<(), EstimationError> {
self.filter.predict(&LocalizationMotion {
delta_arc_length: delta_arc_length.to_f64(),
delta_heading: delta_heading.to_f64(),
})
}
pub fn update<M: OccupancyMap<T>>(
&mut self,
scan: &[T; NUM_BEAMS],
map: &M,
geometry: &ScanGeometry<NUM_BEAMS, T>,
) -> Result<(), EstimationError> {
let beam_model = self.beam_model;
let maximum_range = geometry.maximum_range();
self.filter.update_with_log_weights(|guess| {
let mut score = T::ZERO;
for (index, &measured) in scan.iter().enumerate() {
let Some(offset) = geometry.beam_angle(index) else {
continue;
};
let from_the_map =
map.cast_ray([guess[0], guess[1]], guess[2] + offset, maximum_range);
let believed = geometry.range_is_valid(measured);
score += beam_score(from_the_map, measured, believed, beam_model);
}
score
})
}
pub fn estimate(&self) -> (Vector<3, T>, Matrix<3, 3, T>) {
let particles = self.filter.particles();
let weights = self.filter.weights();
let mut mean_x = T::ZERO;
let mut mean_y = T::ZERO;
let mut sine_sum = T::ZERO;
let mut cosine_sum = T::ZERO;
for (guess, &weight) in particles.iter().zip(weights) {
mean_x += weight * guess[0];
mean_y += weight * guess[1];
sine_sum += weight * guess[2].sin();
cosine_sum += weight * guess[2].cos();
}
let heading = sine_sum.atan2(cosine_sum);
let resultant = sine_sum.hypot(cosine_sum).min(T::ONE);
let heading_spread = ((T::ONE - resultant) * T::TWO).max(T::from_f64(1e-6));
let mut spread_xx = T::ZERO;
let mut spread_yy = T::ZERO;
let mut spread_xy = T::ZERO;
for (guess, &weight) in particles.iter().zip(weights) {
let offset_x = guess[0] - mean_x;
let offset_y = guess[1] - mean_y;
spread_xx += weight * offset_x * offset_x;
spread_yy += weight * offset_y * offset_y;
spread_xy += weight * offset_x * offset_y;
}
let spread = Matrix::from_fn(|row, column| match (row, column) {
(0, 0) => spread_xx,
(1, 1) => spread_yy,
(0, 1) | (1, 0) => spread_xy,
(2, 2) => heading_spread,
_ => T::ZERO,
});
(Vector::new([mean_x, mean_y, heading]), spread)
}
#[must_use]
pub fn is_converged(&self, position_spread_limit: T, heading_spread_limit: T) -> bool {
let (_, spread) = self.estimate();
spread[(0, 0)] + spread[(1, 1)] < position_spread_limit
&& spread[(2, 2)] < heading_spread_limit
}
#[must_use]
pub fn effective_sample_size(&self) -> T {
self.filter.effective_sample_size()
}
#[must_use]
pub fn particle_count(&self) -> usize {
self.filter.particles().len()
}
pub fn particles(&self) -> &[Vector<3, T>] {
self.filter.particles()
}
}
struct LocalizationMotion {
delta_arc_length: f64,
delta_heading: f64,
}
impl VectorFn<3, 3> for LocalizationMotion {
fn eval<S: Numeric>(&self, state: &[S; 3]) -> [S; 3] {
let heading = state[2];
let step = S::from_f64(self.delta_arc_length);
[
state[0] + step * heading.cos(),
state[1] + step * heading.sin(),
(heading + S::from_f64(self.delta_heading)).wrap_to_pi(),
]
}
}
fn beam_score<T: Numeric>(
from_the_map: Option<T>,
measured: T,
believed: bool,
model: BeamModel<T>,
) -> T {
match (from_the_map, believed) {
(Some(range), true) => {
let error = (range - measured) / model.range_deviation;
T::from_f64(-0.5) * error * error
}
(None, false) => model.agreement_reward,
_ => model.mismatch_penalty,
}
}