use std::borrow::Cow;
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use rand::Rng;
use crate::configuration::ProblemSolving;
use crate::error::GaError;
use crate::ga::TerminationCause;
use crate::observer::GaObserver;
use crate::rng::make_rng;
use crate::stats::GenerationStats;
use crate::traits::{FitnessFn, LinearChromosome, RealGene};
use super::configuration::{inertia_weight, PsoConfiguration, PsoTopology};
pub struct PsoResult<U: LinearChromosome> {
pub population: Vec<U>,
pub best: U,
pub best_fitness: f64,
pub generations: usize,
}
struct PsoState {
dim: usize,
n_particles: usize,
velocities: Vec<Vec<f64>>,
pbest_positions: Vec<Vec<f64>>,
pbest_fitness: Vec<f64>,
gbest_position: Vec<f64>,
gbest_fitness: f64,
gbest_owner: usize,
v_max: Vec<f64>,
}
impl PsoState {
fn new<U: LinearChromosome, R: Rng>(pop: &[U], best_idx: usize, rng: &mut R) -> Self
where
U::Gene: RealGene,
{
let n_particles = pop.len();
let dim = pop[0].dna().len();
let v_max: Vec<f64> = (0..dim)
.map(|d| {
pop[0]
.dna()
.get(d)
.and_then(|g| g.bounds())
.map(|(lo, hi)| hi - lo)
.unwrap_or(1.0)
})
.collect();
let velocities: Vec<Vec<f64>> = (0..n_particles)
.map(|_| {
(0..dim)
.map(|d| rng.random::<f64>() * 2.0 * v_max[d] - v_max[d])
.collect()
})
.collect();
let pbest_positions: Vec<Vec<f64>> = pop
.iter()
.map(|ind| ind.dna().iter().map(|g| g.real_value()).collect())
.collect();
let pbest_fitness: Vec<f64> = pop.iter().map(|ind| ind.fitness()).collect();
let gbest_position = pbest_positions[best_idx].clone();
let gbest_fitness = pbest_fitness[best_idx];
PsoState {
dim,
n_particles,
velocities,
pbest_positions,
pbest_fitness,
gbest_position,
gbest_fitness,
gbest_owner: best_idx,
v_max,
}
}
}
pub struct PsoEngine<U: LinearChromosome>
where
U::Gene: RealGene,
{
config: PsoConfiguration,
init_fn: Arc<dyn Fn(usize) -> Vec<U> + Send + Sync>,
fitness_fn: Arc<FitnessFn<U::Gene>>,
observer: Option<Arc<dyn GaObserver<U> + Send + Sync>>,
fitness_cache: Option<Arc<Mutex<crate::fitness::cache::FitnessCache>>>,
}
impl<U: LinearChromosome + Clone> PsoEngine<U>
where
U::Gene: RealGene,
{
pub fn new(
config: PsoConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Self {
Self {
config,
init_fn: Arc::new(init_fn),
fitness_fn: Arc::new(fitness_fn),
observer: None,
fitness_cache: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn GaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
#[inline]
fn notify<F: FnOnce(&dyn GaObserver<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
#[inline]
fn is_better(&self, candidate: f64, current: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => candidate < current,
ProblemSolving::Maximization => candidate > current,
ProblemSolving::FixedFitness => {
if let Some(t) = self.config.fitness_target {
(candidate - t).abs() < (current - t).abs()
} else {
candidate < current
}
}
}
}
fn reached_target(&self, fitness: f64, target: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => fitness <= target,
ProblemSolving::Maximization => fitness >= target,
ProblemSolving::FixedFitness => (fitness - target).abs() < 1e-6,
}
}
fn find_best(&self, pop: &[U]) -> (usize, f64) {
let mut best_idx = 0;
let mut best_fit = pop[0].fitness();
for (i, ind) in pop.iter().enumerate().skip(1) {
if self.is_better(ind.fitness(), best_fit) {
best_fit = ind.fitness();
best_idx = i;
}
}
(best_idx, best_fit)
}
fn lbest_position(
&self,
particle_i: usize,
gene_d: usize,
neighborhood_size: usize,
state: &PsoState,
) -> f64 {
let n = state.n_particles;
let k = neighborhood_size.min(n - 1).max(1);
let half_left = k / 2;
let half_right = k.div_ceil(2);
let mut best_idx = particle_i;
let mut best_fit = state.pbest_fitness[particle_i];
for offset in 1..=half_left {
let j = (particle_i + n - offset) % n;
if self.is_better(state.pbest_fitness[j], best_fit) {
best_fit = state.pbest_fitness[j];
best_idx = j;
}
}
for offset in 1..=half_right {
let j = (particle_i + offset) % n;
if self.is_better(state.pbest_fitness[j], best_fit) {
best_fit = state.pbest_fitness[j];
best_idx = j;
}
}
state.pbest_positions[best_idx][gene_d]
}
pub fn run(&mut self) -> Result<PsoResult<U>, GaError>
where
U::Gene: Debug,
{
let mut rng = make_rng();
let is_maximization = matches!(self.config.problem_solving, ProblemSolving::Maximization);
if let Some(size) = self.config.fitness_cache_size {
if self.fitness_cache.is_none() {
let (wrapped_fn, cache_handle) =
crate::fitness::cache::wrap_with_cache(Arc::clone(&self.fitness_fn), size);
self.fitness_fn = wrapped_fn;
self.fitness_cache = Some(cache_handle);
}
}
let pop_size = if self.config.population_size == 0 {
30
} else {
self.config.population_size
};
let mut pop: Vec<U> = (self.init_fn)(pop_size.max(1));
if pop.is_empty() {
return Err(GaError::InitializationError(
"PsoEngine: init_fn returned an empty population".to_string(),
));
}
self.notify(|obs| obs.on_run_start());
for ind in &mut pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
let (best_idx, mut best_fitness) = self.find_best(&pop);
let mut best = pop[best_idx].clone();
let mut state = PsoState::new(&pop, best_idx, &mut rng);
self.notify(|obs| obs.on_new_best(0, &best));
let mut termination_cause = TerminationCause::GenerationLimitReached;
let mut all_stats: Vec<GenerationStats> = Vec::with_capacity(self.config.max_generations);
let (mut prev_cache_hits, mut prev_cache_misses) = match &self.fitness_cache {
Some(ch) => {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
(c.hits(), c.misses())
}
None => (0, 0),
};
for gen in 0..self.config.max_generations {
self.notify(|obs| obs.on_generation_start(gen));
let w = inertia_weight(&self.config.inertia, gen, self.config.max_generations);
#[allow(clippy::needless_range_loop)]
for i in 0..pop.len() {
let x_curr: Vec<f64> = pop[i].dna().iter().map(|g| g.real_value()).collect();
let mut new_positions = x_curr.clone();
for d in 0..state.dim {
let r1: f64 = rng.random();
let r2: f64 = rng.random();
let best_d = match &self.config.topology {
PsoTopology::Global => state.gbest_position[d],
PsoTopology::Ring { neighborhood_size } => {
self.lbest_position(i, d, *neighborhood_size, &state)
}
};
let mut new_v = w * state.velocities[i][d]
+ self.config.c1 * r1 * (state.pbest_positions[i][d] - x_curr[d])
+ self.config.c2 * r2 * (best_d - x_curr[d]);
new_v = new_v.clamp(-state.v_max[d], state.v_max[d]);
let mut new_x = x_curr[d] + new_v;
if let Some((lo, hi)) = pop[i].dna()[d].bounds() {
if new_x < lo {
new_x = lo;
new_v = 0.0;
} else if new_x > hi {
new_x = hi;
new_v = 0.0;
}
}
state.velocities[i][d] = new_v;
new_positions[d] = new_x;
}
let new_dna: Vec<U::Gene> = pop[i]
.dna()
.iter()
.enumerate()
.map(|(d, g)| g.with_real_value(new_positions[d]))
.collect();
pop[i].set_dna(Cow::Owned(new_dna));
let new_fit = (self.fitness_fn)(pop[i].dna());
pop[i].set_fitness(new_fit);
if self.is_better(new_fit, state.pbest_fitness[i]) {
state.pbest_fitness[i] = new_fit;
state.pbest_positions[i] = new_positions.clone();
}
}
for j in 0..state.n_particles {
if self.is_better(state.pbest_fitness[j], state.gbest_fitness) {
state.gbest_fitness = state.pbest_fitness[j];
state.gbest_position = state.pbest_positions[j].clone();
state.gbest_owner = j;
}
}
if self.is_better(state.gbest_fitness, best_fitness) {
best_fitness = state.gbest_fitness;
let owner = state.gbest_owner;
let new_dna: Vec<U::Gene> = pop[owner]
.dna()
.iter()
.enumerate()
.map(|(d, g)| g.with_real_value(state.gbest_position[d]))
.collect();
best = pop[owner].clone();
best.set_dna(Cow::Owned(new_dna));
best.set_fitness(state.gbest_fitness);
self.notify(|obs| obs.on_new_best(gen, &best));
}
let fitness_values: Vec<f64> = pop.iter().map(|c| c.fitness()).collect();
let mut stats =
GenerationStats::from_fitness_values(gen, &fitness_values, is_maximization);
if let Some(ref ch) = self.fitness_cache {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
stats.cache_hits = Some(c.hits().saturating_sub(prev_cache_hits));
stats.cache_misses = Some(c.misses().saturating_sub(prev_cache_misses));
prev_cache_hits = c.hits();
prev_cache_misses = c.misses();
}
self.notify(|obs| obs.on_generation_end(&stats));
all_stats.push(stats);
if let Some(target) = self.config.fitness_target {
if self.reached_target(best_fitness, target) {
termination_cause = TerminationCause::FitnessTargetReached;
break;
}
}
}
let generations = all_stats.len();
let all_stats_ref = all_stats.as_slice();
self.notify(|obs| obs.on_run_end(termination_cause, all_stats_ref));
let verified_fitness = (self.fitness_fn)(best.dna());
best.set_fitness(verified_fitness);
Ok(PsoResult {
population: pop,
best,
best_fitness: verified_fitness,
generations,
})
}
}