use crate::core::constraint::evaluation::BoxProblem;
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::PopulationProgress;
use crate::core::termination::Termination;
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: Scalar = 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: Scalar = f64> {
Unbounded,
SpanFraction(F),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct GlobalBestPso<V, F: Scalar = f64, R = ChaCha8Rng> {
swarm_size_override: Option<usize>,
inertia: F,
cognitive: F,
social: F,
boundary_handling: PsoBoundaryHandling<F>,
velocity_limit: PsoVelocityLimit<F>,
rng: R,
initial_velocities: Option<Vec<V>>,
live_rng: Option<R>,
velocities: Vec<V>,
personal_best_positions: Vec<V>,
personal_best_costs: Vec<F>,
global_best_position: Option<V>,
global_best_cost: F,
cognitive_draws: Vec<F>,
social_draws: Vec<F>,
}
impl<V, F: Scalar> GlobalBestPso<V, F, ChaCha8Rng> {
pub fn new(seed: u64) -> Self {
Self::new_with_rng(ChaCha8Rng::seed_from_u64(seed))
}
}
impl<V, F: Scalar, R> GlobalBestPso<V, 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,
initial_velocities: None,
live_rng: None,
velocities: Vec::new(),
personal_best_positions: Vec::new(),
personal_best_costs: Vec::new(),
global_best_position: None,
global_best_cost: F::infinity(),
cognitive_draws: Vec::new(),
social_draws: Vec::new(),
}
}
pub fn with_initial_velocities(
mut self,
velocities: Option<Vec<V>>,
) -> Self {
if let Some(values) = &velocities {
assert!(
!values.is_empty(),
"GlobalBestPso requires at least one initial velocity"
);
}
self.initial_velocities = velocities;
self
}
pub fn velocities(&self) -> &[V] {
&self.velocities
}
pub fn personal_best_positions(&self) -> &[V] {
&self.personal_best_positions
}
pub fn personal_best_costs(&self) -> &[F] {
&self.personal_best_costs
}
pub fn global_best(&self) -> Option<(&V, F)> {
self.global_best_position
.as_ref()
.filter(|_| usable_cost(self.global_best_cost))
.map(|point| (point, self.global_best_cost))
}
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];
}
}
}
}
}
impl<V, F: Scalar, R> GlobalBestPso<V, F, R> {
fn sort_particles_ascending(
&mut self,
state: &mut PopulationProgress<V, F>,
) {
let n = state.candidates.len();
debug_assert_eq!(state.costs.len(), n);
debug_assert_eq!(self.velocities.len(), n);
debug_assert_eq!(self.personal_best_positions.len(), n);
debug_assert_eq!(self.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.candidates, &order);
apply_permutation(&mut state.costs, &order);
apply_permutation(&mut self.velocities, &order);
apply_permutation(&mut self.personal_best_positions, &order);
apply_permutation(&mut self.personal_best_costs, &order);
}
}
impl<P, V, F, R> Solver<P, PopulationProgress<V, F>> for GlobalBestPso<V, F, R>
where
F: Scalar + crate::core::parallel::MaybeSend,
P: CostFunction<Param = V, Output = F>
+ BoxProblem<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,
V: crate::VectorLen + std::ops::Index<usize, Output = F>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<PopulationProgress<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_box_problem(
problem.inner(),
None,
true,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
state.reset();
self.velocities.clear();
self.personal_best_positions.clear();
self.personal_best_costs.clear();
self.global_best_position = None;
self.global_best_cost = F::infinity();
self.live_rng = None;
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.candidates.is_empty();
let swarm_size = if supplied_positions {
let supplied = state.candidates.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.candidates.reserve(swarm_size);
for _ in 0..swarm_size {
state
.candidates
.push(V::sample_uniform_box(&lower, &upper, &mut rng));
}
}
for (i, position) in state.candidates.iter().enumerate() {
validate_particle_shape(position, dimension, "position", i);
}
if let Some(initial) = &self.initial_velocities {
self.velocities.clone_from(initial);
}
if self.velocities.is_empty() {
self.velocities.reserve(swarm_size);
for position in &state.candidates {
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];
}
self.velocities.push(velocity);
}
} else {
assert_eq!(
self.velocities.len(),
swarm_size,
"GlobalBestPso requires one velocity per position"
);
for (i, velocity) in self.velocities.iter().enumerate() {
validate_particle_shape(velocity, dimension, "velocity", i);
}
}
for (position, velocity) in
state.candidates.iter_mut().zip(&mut self.velocities)
{
repair_position(
position,
velocity,
&lower,
&upper,
self.boundary_handling,
);
}
state.costs = problem.cost_batch(&state.candidates)?;
self.personal_best_positions = state.candidates.clone();
self.personal_best_costs = state.costs.clone();
self.sort_particles_ascending(&mut state);
let best = state
.costs
.iter()
.position(|&cost| usable_cost(cost))
.unwrap_or(0);
self.global_best_position = Some(state.candidates[best].clone());
self.global_best_cost = state.costs[best];
self.live_rng = Some(rng);
self.cognitive_draws.resize(dimension, F::zero());
self.social_draws.resize(dimension, F::zero());
self.cognitive_draws.fill(F::zero());
self.social_draws.fill(F::zero());
state
.publish_representative_from(
self.global_best_position.as_ref().unwrap(),
self.global_best_cost,
)
.expect("initialized swarm has matching member shapes");
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<crate::SolverStep<PopulationProgress<V, F>>, Self::Error> {
let lower = problem.inner().lower().clone();
let upper = problem.inner().upper().clone();
let global_best = self
.global_best_position
.as_ref()
.expect("GlobalBestPso::init must run before next_iter")
.clone();
let rng = self
.live_rng
.as_mut()
.expect("GlobalBestPso::init must seed the live RNG");
let dimension = lower.vec_len();
for i in 0..state.candidates.len() {
for draw in &mut self.cognitive_draws {
*draw = F::from_f64(rng.random::<f64>()).unwrap();
}
for draw in &mut self.social_draws {
*draw = F::from_f64(rng.random::<f64>()).unwrap();
}
for j in 0..dimension {
let position = state.candidates[i][j];
let raw_velocity = self.inertia * self.velocities[i][j]
+ self.cognitive
* self.cognitive_draws[j]
* (self.personal_best_positions[i][j] - position)
+ self.social
* self.social_draws[j]
* (global_best[j] - position);
if !raw_velocity.is_finite() {
self.velocities[i][j] = F::zero();
continue;
}
self.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.candidates[i][j] = position + self.velocities[i][j];
}
repair_position(
&mut state.candidates[i],
&mut self.velocities[i],
&lower,
&upper,
self.boundary_handling,
);
}
state.costs = problem.cost_batch(&state.candidates)?;
for i in 0..state.candidates.len() {
if strictly_better(state.costs[i], self.personal_best_costs[i]) {
self.personal_best_positions[i] = state.candidates[i].clone();
self.personal_best_costs[i] = state.costs[i];
}
if strictly_better(
self.personal_best_costs[i],
self.global_best_cost,
) {
self.global_best_position =
Some(self.personal_best_positions[i].clone());
self.global_best_cost = self.personal_best_costs[i];
}
}
self.sort_particles_ascending(&mut state);
state
.publish_representative_from(
self.global_best_position.as_ref().unwrap(),
self.global_best_cost,
)
.expect("updated swarm has matching member shapes");
Ok(crate::SolverStep::from((state, None)))
}
fn terminate(
&self,
_state: &PopulationProgress<V, F>,
) -> Option<Termination<F>> {
let cost = self.global_best_cost;
if !usable_cost(cost) {
Some(Termination::numerical_failure(
"The swarm has no incumbent with a usable objective value.",
))
} else if cost == F::neg_infinity() {
Some(Termination::custom(
"global_best_pso.negative_infinity",
"The published incumbent objective equals negative infinity.",
vec![crate::Measurement {
name: "cost".into(),
value: cost,
}],
))
} 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::<Vec<f64>>::new(0).with_swarm_size(0);
}
#[test]
#[should_panic(expected = "inertia must be finite and nonnegative")]
fn negative_inertia_is_rejected() {
let _ = GlobalBestPso::<Vec<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::<Vec<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::<Vec<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]);
}
}