use crate::{SolverError, SolverResult, VectorType, DEFAULT_ITERMAX, DEFAULT_TOL};
use nalgebra::{allocator::Allocator, ComplexField, DefaultAllocator, Dim, Scalar};
use num_traits::Float;
use rand::rng;
use rand_distr::{uniform::SampleUniform, Distribution, Uniform};
use std::{marker::PhantomData, slice::Iter};
const DEFAULT_INERTIA_WEIGHT: f64 = 0.5;
const DEFAULT_COGNIIVE_COEFFICIENT: f64 = 1.0;
const DEFAULT_SOCIAL_COEFFICIENT: f64 = 1.0;
const DEFAULT_PARTICLE_COUNT: usize = 100;
const DEFAULT_STALL_ITERATIONS: usize = 50;
struct Particle<T: Scalar, D: Dim>
where
DefaultAllocator: Allocator<D>,
{
pub position: VectorType<T, D>,
pub velocity: VectorType<T, D>,
pub best_position: VectorType<T, D>,
pub best_cost: T,
}
struct CircularArray<T, const N: usize> {
array: [T; N],
i: usize,
}
impl<T: Copy, const N: usize> CircularArray<T, N> {
pub fn fill(value: T) -> Self {
Self {
array: [value; N],
i: 0,
}
}
pub fn push(&mut self, value: T) {
self.array[self.i] = value;
self.i += 1;
self.i %= N;
}
pub fn iter(&self) -> Iter<'_, T> {
self.array.iter()
}
}
pub struct ParticleSwarm<T: Scalar + SampleUniform, D: Dim, F>
where
F: Fn(VectorType<T, D>) -> T,
DefaultAllocator: Allocator<D>,
{
f: F,
inertia_weight: T,
cognitive_coefficient: T,
social_coefficient: T,
particle_count: usize,
tolerance: T,
iter_max: usize,
position_distributions: Vec<Uniform<T>>,
velocity_distributions: Vec<Uniform<T>>,
d_phantom: PhantomData<D>,
}
impl<T, D, F> ParticleSwarm<T, D, F>
where
T: Scalar + Float + SampleUniform + ComplexField<RealField = T>,
D: Dim,
F: Fn(VectorType<T, D>) -> T,
DefaultAllocator: Allocator<D>,
{
pub fn new(
f: F,
lower_bounds: VectorType<T, D>,
upper_bounds: VectorType<T, D>,
) -> SolverResult<Self> {
let position_distributions = lower_bounds
.iter()
.zip(upper_bounds.iter())
.map(|(&low, &up)| Uniform::new_inclusive(low, up))
.collect::<Result<Vec<_>, _>>()
.map_err(|error| SolverError::ExternalError(error.to_string()))?;
let velocity_distributions = lower_bounds
.iter()
.zip(upper_bounds.iter())
.map(|(&low, &up)| {
let distance = Float::abs(up - low);
Uniform::new_inclusive(-distance, distance)
})
.collect::<Result<Vec<_>, _>>()
.map_err(|error| SolverError::ExternalError(error.to_string()))?;
SolverResult::Ok(Self {
f,
inertia_weight: T::from(DEFAULT_INERTIA_WEIGHT).unwrap(),
cognitive_coefficient: T::from(DEFAULT_COGNIIVE_COEFFICIENT).unwrap(),
social_coefficient: T::from(DEFAULT_SOCIAL_COEFFICIENT).unwrap(),
tolerance: T::from(DEFAULT_TOL).unwrap(),
iter_max: DEFAULT_ITERMAX,
particle_count: DEFAULT_PARTICLE_COUNT,
position_distributions,
velocity_distributions,
d_phantom: PhantomData,
})
}
pub fn with_inertia_weight(&mut self, inertia_weight: T) -> &mut Self {
self.inertia_weight = inertia_weight;
self
}
pub fn with_cognitive_coefficient(&mut self, cognitive_coefficient: T) -> &mut Self {
self.cognitive_coefficient = cognitive_coefficient;
self
}
pub fn with_social_coefficient(&mut self, social_coefficient: T) -> &mut Self {
self.social_coefficient = social_coefficient;
self
}
pub fn with_tol(&mut self, tolerance: T) -> &mut Self {
self.tolerance = tolerance;
self
}
pub fn with_particle_count(&mut self, particle_count: usize) -> &mut Self {
self.particle_count = particle_count;
self
}
pub fn with_iter_max(&mut self, iter_max: usize) -> &mut Self {
self.iter_max = iter_max;
self
}
pub fn solve(&self, x0: VectorType<T, D>) -> SolverResult<VectorType<T, D>> {
let mut global_best_position = x0.clone();
let mut global_best_cost = (self.f)(global_best_position.clone());
let mut previous_best = CircularArray::fill(T::infinity());
let mut particles: Vec<Particle<T, D>> = (0..self.particle_count)
.map(|_| {
let mut position = x0.clone();
position
.iter_mut()
.zip(self.position_distributions.iter())
.for_each(|(v, dist)| *v = dist.sample(&mut rng()));
let mut velocity = x0.clone();
velocity
.iter_mut()
.zip(self.velocity_distributions.iter())
.for_each(|(v, dist)| *v = dist.sample(&mut rng()));
let cost = (self.f)(position.clone());
if cost < global_best_cost {
previous_best.push(global_best_cost);
global_best_position = position.clone();
global_best_cost = cost;
}
Particle {
position: position.clone(),
velocity,
best_position: position,
best_cost: cost,
}
})
.collect();
let u01 = Uniform::new_inclusive(T::zero(), T::one())
.map_err(|error| SolverError::ExternalError(error.to_string()))?;
for _ in 0..self.iter_max {
if self.stalled_too_long(global_best_cost, &previous_best) {
break;
}
for Particle {
position,
velocity,
best_position,
best_cost,
} in particles.iter_mut()
{
for (d, v_d) in velocity.iter_mut().enumerate() {
let rp = u01.sample(&mut rng());
let rg = u01.sample(&mut rng());
*v_d = self.inertia_weight * *v_d
+ self.cognitive_coefficient * rp * (best_position[d] - position[d])
+ self.social_coefficient * rg * (global_best_position[d] - position[d]);
}
*position += &*velocity;
let cost = (self.f)(position.clone());
if cost < *best_cost {
*best_position = position.clone();
*best_cost = cost;
if *best_cost < global_best_cost {
previous_best.push(global_best_cost);
global_best_position = position.clone();
global_best_cost = *best_cost;
}
}
}
}
Ok(global_best_position)
}
fn stalled_too_long(
&self,
global_best: T,
previous: &CircularArray<T, DEFAULT_STALL_ITERATIONS>,
) -> bool {
previous
.iter()
.all(|&x| Float::abs(x - global_best) < self.tolerance)
}
}