use crate::core::constraint::BoxConstraints;
use crate::core::math::{SampleUniformBox, Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, Rng, RngExt, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::GlobalBestPsoState;
use crate::core::termination::TerminationReason;
use crate::solver::cma_es::{apply_permutation, nan_last_cmp};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PsoBoundaryHandling<F = f64> {
Absorb,
Preserve,
Reflect {
damping: F,
},
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PsoVelocityLimit<F = f64> {
Unbounded,
SpanFraction(F),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct GlobalBestPso<F = f64, R = ChaCha8Rng> {
swarm_size_override: Option<usize>,
inertia: F,
cognitive: F,
social: F,
boundary_handling: PsoBoundaryHandling<F>,
velocity_limit: PsoVelocityLimit<F>,
rng: R,
}
impl<F: Scalar> GlobalBestPso<F, ChaCha8Rng> {
pub fn new(seed: u64) -> Self {
Self::new_with_rng(ChaCha8Rng::seed_from_u64(seed))
}
}
impl<F: Scalar, R> GlobalBestPso<F, R> {
pub fn new_with_rng(rng: R) -> Self {
let two = F::from_f64(2.0).unwrap();
Self {
swarm_size_override: None,
inertia: F::one() / (two * two.ln()),
cognitive: F::from_f64(0.5).unwrap() + two.ln(),
social: F::from_f64(0.5).unwrap() + two.ln(),
boundary_handling: PsoBoundaryHandling::Absorb,
velocity_limit: PsoVelocityLimit::Unbounded,
rng,
}
}
pub fn default_swarm_size(dimension: usize) -> usize {
10 + (2.0 * (dimension as f64).sqrt()).floor() as usize
}
pub fn inertia(&self) -> F {
self.inertia
}
pub fn cognitive(&self) -> F {
self.cognitive
}
pub fn social(&self) -> F {
self.social
}
pub fn with_swarm_size(mut self, swarm_size: usize) -> Self {
assert!(swarm_size >= 1, "GlobalBestPso requires swarm_size >= 1");
self.swarm_size_override = Some(swarm_size);
self
}
pub fn with_inertia(mut self, inertia: F) -> Self {
assert_nonnegative_finite("inertia", inertia);
self.inertia = inertia;
self
}
pub fn with_cognitive(mut self, cognitive: F) -> Self {
assert_nonnegative_finite("cognitive coefficient", cognitive);
self.cognitive = cognitive;
self
}
pub fn with_social(mut self, social: F) -> Self {
assert_nonnegative_finite("social coefficient", social);
self.social = social;
self
}
pub fn with_boundary_handling(
mut self,
boundary_handling: PsoBoundaryHandling<F>,
) -> Self {
if let PsoBoundaryHandling::Reflect { damping } = boundary_handling {
assert!(
damping.is_finite()
&& damping >= F::zero()
&& damping <= F::one(),
"GlobalBestPso reflection damping must be finite and in [0, 1], got {damping:?}"
);
}
self.boundary_handling = boundary_handling;
self
}
pub fn with_velocity_limit(
mut self,
velocity_limit: PsoVelocityLimit<F>,
) -> Self {
if let PsoVelocityLimit::SpanFraction(fraction) = velocity_limit {
assert_nonnegative_finite("velocity span fraction", fraction);
}
self.velocity_limit = velocity_limit;
self
}
}
fn assert_nonnegative_finite<F: Scalar>(name: &str, value: F) {
assert!(
value.is_finite() && value >= F::zero(),
"GlobalBestPso {name} must be finite and nonnegative, got {value:?}"
);
}
fn usable_cost<F: Scalar>(cost: F) -> bool {
cost < F::infinity()
}
fn strictly_better<F: Scalar>(candidate: F, incumbent: F) -> bool {
usable_cost(candidate) && (!usable_cost(incumbent) || candidate < incumbent)
}
fn validate_box<V, F>(lower: &V, upper: &V)
where
V: VectorLen + std::ops::Index<usize, Output = F>,
F: Scalar,
{
assert_eq!(
lower.vec_len(),
upper.vec_len(),
"GlobalBestPso requires lower and upper bounds of equal length"
);
assert!(
lower.vec_len() > 0,
"GlobalBestPso requires a non-empty search box"
);
for j in 0..lower.vec_len() {
assert!(
lower[j].is_finite() && upper[j].is_finite(),
"GlobalBestPso requires finite bounds, got lower[{j}] = {:?}, upper[{j}] = {:?}",
lower[j],
upper[j]
);
assert!(
lower[j] <= upper[j],
"GlobalBestPso requires lower <= upper, got lower[{j}] = {:?}, upper[{j}] = {:?}",
lower[j],
upper[j]
);
}
}
fn validate_particle_shape<V, F>(
value: &V,
dimension: usize,
kind: &str,
particle: usize,
) where
V: VectorLen + std::ops::Index<usize, Output = F>,
F: Scalar,
{
assert_eq!(
value.vec_len(),
dimension,
"GlobalBestPso {kind} {particle} has dimension {}, expected {dimension}",
value.vec_len()
);
for j in 0..dimension {
assert!(
value[j].is_finite(),
"GlobalBestPso {kind} {particle} contains a non-finite coordinate at index {j}"
);
}
}
fn repair_position<V, F>(
position: &mut V,
velocity: &mut V,
lower: &V,
upper: &V,
handling: PsoBoundaryHandling<F>,
) where
V: VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
F: Scalar,
{
for j in 0..position.vec_len() {
let crossed = if position[j] < lower[j] {
position[j] = lower[j];
true
} else if position[j] > upper[j] {
position[j] = upper[j];
true
} else {
false
};
if crossed {
match handling {
PsoBoundaryHandling::Absorb => velocity[j] = F::zero(),
PsoBoundaryHandling::Preserve => {}
PsoBoundaryHandling::Reflect { damping } => {
velocity[j] = -damping * velocity[j];
}
}
}
}
}
fn sort_particles_ascending<V, F: PartialOrd, R>(
state: &mut GlobalBestPsoState<V, F, R>,
) {
let n = state.positions.len();
debug_assert_eq!(state.costs.len(), n);
debug_assert_eq!(state.velocities.len(), n);
debug_assert_eq!(state.personal_best_positions.len(), n);
debug_assert_eq!(state.personal_best_costs.len(), n);
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&i, &j| nan_last_cmp(&state.costs[i], &state.costs[j]));
apply_permutation(&mut state.positions, &order);
apply_permutation(&mut state.costs, &order);
apply_permutation(&mut state.velocities, &order);
apply_permutation(&mut state.personal_best_positions, &order);
apply_permutation(&mut state.personal_best_costs, &order);
}
impl<P, V, F, R> Solver<P, GlobalBestPsoState<V, F, R>> for GlobalBestPso<F, R>
where
F: Scalar + crate::core::parallel::MaybeSend,
P: CostFunction<Param = V, Output = F>
+ BoxConstraints<Param = V>
+ crate::core::parallel::MaybeSync,
P::Error: crate::core::parallel::MaybeSend,
V: Clone
+ VectorLen
+ SampleUniformBox
+ crate::core::parallel::MaybeSync
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
R: Rng + Clone,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: GlobalBestPsoState<V, F, R>,
) -> Result<GlobalBestPsoState<V, F, R>, Self::Error> {
if state.initialized {
return Ok(state);
}
let lower = problem.inner().lower().clone();
let upper = problem.inner().upper().clone();
validate_box(&lower, &upper);
let dimension = lower.vec_len();
let supplied_positions = !state.positions.is_empty();
let swarm_size = if supplied_positions {
let supplied = state.positions.len();
if let Some(configured) = self.swarm_size_override {
assert_eq!(
supplied, configured,
"GlobalBestPso warm start has {supplied} particles, but the configured swarm size is {configured}"
);
}
supplied
} else {
self.swarm_size_override
.unwrap_or_else(|| Self::default_swarm_size(dimension))
};
assert!(swarm_size >= 1, "GlobalBestPso requires swarm_size >= 1");
let mut rng = self.rng.clone();
if !supplied_positions {
state.positions.reserve(swarm_size);
for _ in 0..swarm_size {
state
.positions
.push(V::sample_uniform_box(&lower, &upper, &mut rng));
}
}
for (i, position) in state.positions.iter().enumerate() {
validate_particle_shape(position, dimension, "position", i);
}
if state.velocities.is_empty() {
state.velocities.reserve(swarm_size);
for position in &state.positions {
let target = V::sample_uniform_box(&lower, &upper, &mut rng);
let mut velocity = target;
let half = F::from_f64(0.5).unwrap();
for j in 0..dimension {
velocity[j] = half * velocity[j] - half * position[j];
}
state.velocities.push(velocity);
}
} else {
assert_eq!(
state.velocities.len(),
swarm_size,
"GlobalBestPso requires one velocity per position"
);
for (i, velocity) in state.velocities.iter().enumerate() {
validate_particle_shape(velocity, dimension, "velocity", i);
}
}
for (position, velocity) in
state.positions.iter_mut().zip(&mut state.velocities)
{
repair_position(
position,
velocity,
&lower,
&upper,
self.boundary_handling,
);
}
state.costs = problem.cost_batch(&state.positions)?;
state.personal_best_positions = state.positions.clone();
state.personal_best_costs = state.costs.clone();
sort_particles_ascending(&mut state);
let best = state
.costs
.iter()
.position(|&cost| usable_cost(cost))
.unwrap_or(0);
state.global_best_position = Some(state.positions[best].clone());
state.global_best_cost = state.costs[best];
state.rng = Some(rng);
state.initialized = true;
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: GlobalBestPsoState<V, F, R>,
) -> Result<
(GlobalBestPsoState<V, F, R>, Option<TerminationReason>),
Self::Error,
> {
let lower = problem.inner().lower().clone();
let upper = problem.inner().upper().clone();
let global_best = state
.global_best_position
.as_ref()
.expect("GlobalBestPso::init must run before next_iter")
.clone();
let rng = state
.rng
.as_mut()
.expect("GlobalBestPso::init must seed the state RNG");
let dimension = lower.vec_len();
let mut cognitive_draws = vec![F::zero(); dimension];
let mut social_draws = vec![F::zero(); dimension];
for i in 0..state.positions.len() {
for draw in &mut cognitive_draws {
*draw = F::from_f64(rng.random::<f64>()).unwrap();
}
for draw in &mut social_draws {
*draw = F::from_f64(rng.random::<f64>()).unwrap();
}
for j in 0..dimension {
let position = state.positions[i][j];
let raw_velocity = self.inertia * state.velocities[i][j]
+ self.cognitive
* cognitive_draws[j]
* (state.personal_best_positions[i][j] - position)
+ self.social
* social_draws[j]
* (global_best[j] - position);
if !raw_velocity.is_finite() {
state.velocities[i][j] = F::zero();
continue;
}
state.velocities[i][j] = match self.velocity_limit {
PsoVelocityLimit::Unbounded => raw_velocity,
PsoVelocityLimit::SpanFraction(fraction) => {
let span = upper[j] - lower[j];
let limit = if span.is_finite() {
fraction * span
} else {
fraction * upper[j] - fraction * lower[j]
};
raw_velocity.max(-limit).min(limit)
}
};
state.positions[i][j] = position + state.velocities[i][j];
}
repair_position(
&mut state.positions[i],
&mut state.velocities[i],
&lower,
&upper,
self.boundary_handling,
);
}
state.costs = problem.cost_batch(&state.positions)?;
for i in 0..state.positions.len() {
if strictly_better(state.costs[i], state.personal_best_costs[i]) {
state.personal_best_positions[i] = state.positions[i].clone();
state.personal_best_costs[i] = state.costs[i];
}
if strictly_better(
state.personal_best_costs[i],
state.global_best_cost,
) {
state.global_best_position =
Some(state.personal_best_positions[i].clone());
state.global_best_cost = state.personal_best_costs[i];
}
}
sort_particles_ascending(&mut state);
Ok((state, None))
}
fn terminate(
&self,
state: &GlobalBestPsoState<V, F, R>,
) -> Option<TerminationReason> {
let cost = state.global_best_cost;
if !usable_cost(cost) {
Some(TerminationReason::SolverFailed)
} else if cost == F::neg_infinity() {
Some(TerminationReason::SolverConverged)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_improvement_replaces_nan_and_infinity() {
assert!(strictly_better(1.0, f64::NAN));
assert!(strictly_better(1.0, f64::INFINITY));
assert!(!strictly_better(f64::NAN, 1.0));
assert!(!strictly_better(f64::INFINITY, 1.0));
assert!(!strictly_better(1.0, 1.0));
}
#[test]
#[should_panic(expected = "swarm_size >= 1")]
fn zero_swarm_size_is_rejected() {
let _ = GlobalBestPso::<f64>::new(0).with_swarm_size(0);
}
#[test]
#[should_panic(expected = "inertia must be finite and nonnegative")]
fn negative_inertia_is_rejected() {
let _ = GlobalBestPso::<f64>::new(0).with_inertia(-0.1);
}
#[test]
#[should_panic(
expected = "reflection damping must be finite and in [0, 1]"
)]
fn invalid_reflection_damping_is_rejected() {
let _ = GlobalBestPso::<f64>::new(0).with_boundary_handling(
PsoBoundaryHandling::Reflect { damping: 1.1 },
);
}
#[test]
#[should_panic(
expected = "velocity span fraction must be finite and nonnegative"
)]
fn negative_velocity_fraction_is_rejected() {
let _ = GlobalBestPso::<f64>::new(0)
.with_velocity_limit(PsoVelocityLimit::SpanFraction(-0.1));
}
#[test]
#[should_panic(expected = "requires a non-empty search box")]
fn empty_box_is_rejected() {
validate_box::<Vec<f64>, f64>(&vec![], &vec![]);
}
#[test]
#[should_panic(expected = "requires lower <= upper")]
fn unordered_box_is_rejected() {
validate_box(&vec![1.0], &vec![-1.0]);
}
}