1use crate::error::OptimizeError;
17use crate::unconstrained::OptimizeResult;
18use scirs2_core::ndarray::{Array1, ArrayView1};
19use scirs2_core::random::rngs::StdRng;
20use scirs2_core::random::{rng, Rng, RngExt, SeedableRng};
21
22#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum Topology {
25 Star,
27 Ring,
29}
30
31impl Default for Topology {
32 fn default() -> Self {
33 Topology::Star
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum VelocityStrategy {
40 InertiaWeight,
42 Constriction,
44}
45
46impl Default for VelocityStrategy {
47 fn default() -> Self {
48 VelocityStrategy::InertiaWeight
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct ParticleSwarmOptions {
55 pub swarm_size: usize,
57 pub maxiter: usize,
59 pub c1: f64,
61 pub c2: f64,
63 pub w: f64,
65 pub vmin: f64,
67 pub vmax: f64,
69 pub tol: f64,
71 pub seed: Option<u64>,
73 pub adaptive: bool,
75 pub topology: Topology,
77 pub velocity_strategy: VelocityStrategy,
79 pub stagnation_limit: usize,
81 pub reinit_fraction: f64,
83 pub ring_neighbors: usize,
85}
86
87impl Default for ParticleSwarmOptions {
88 fn default() -> Self {
89 Self {
90 swarm_size: 50,
91 maxiter: 500,
92 c1: 2.0,
93 c2: 2.0,
94 w: 0.9,
95 vmin: -0.5,
96 vmax: 0.5,
97 tol: 1e-8,
98 seed: None,
99 adaptive: false,
100 topology: Topology::Star,
101 velocity_strategy: VelocityStrategy::InertiaWeight,
102 stagnation_limit: 20,
103 reinit_fraction: 0.2,
104 ring_neighbors: 3,
105 }
106 }
107}
108
109pub type Bounds = Vec<(f64, f64)>;
111
112#[derive(Debug, Clone)]
114struct Particle {
115 position: Array1<f64>,
117 velocity: Array1<f64>,
119 best_position: Array1<f64>,
121 best_value: f64,
123}
124
125pub struct ParticleSwarm<F>
127where
128 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
129{
130 func: F,
131 bounds: Bounds,
132 options: ParticleSwarmOptions,
133 ndim: usize,
134 particles: Vec<Particle>,
135 global_best_position: Array1<f64>,
136 global_best_value: f64,
137 rng: StdRng,
138 nfev: usize,
139 iteration: usize,
140 inertia_weight: f64,
141 constriction_chi: f64,
143 stagnation_counter: usize,
145 prev_global_best: f64,
147}
148
149impl<F> ParticleSwarm<F>
150where
151 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
152{
153 pub fn new(func: F, bounds: Bounds, options: ParticleSwarmOptions) -> Self {
155 let ndim = bounds.len();
156 let seed = options.seed.unwrap_or_else(|| rng().random());
157 let mut rng_gen = StdRng::seed_from_u64(seed);
158
159 let phi = options.c1 + options.c2;
161 let constriction_chi = if phi > 4.0 {
162 2.0 / (phi - 2.0 + (phi * phi - 4.0 * phi).sqrt()).abs()
163 } else {
164 0.7298 };
166
167 let mut particles = Vec::with_capacity(options.swarm_size);
169 let mut global_best_position = Array1::zeros(ndim);
170 let mut global_best_value = f64::INFINITY;
171 let mut nfev = 0;
172
173 for _ in 0..options.swarm_size {
174 let mut position = Array1::zeros(ndim);
176 let mut velocity = Array1::zeros(ndim);
177
178 for j in 0..ndim {
179 let (lb, ub) = bounds[j];
180 position[j] = rng_gen.random_range(lb..ub);
181 velocity[j] = rng_gen.random_range(options.vmin..options.vmax) * (ub - lb);
182 }
183
184 let value = func(&position.view());
186 nfev += 1;
187
188 if value < global_best_value {
190 global_best_value = value;
191 global_best_position = position.clone();
192 }
193
194 particles.push(Particle {
195 position: position.clone(),
196 velocity,
197 best_position: position,
198 best_value: value,
199 });
200 }
201
202 Self {
203 func,
204 bounds,
205 options: options.clone(),
206 ndim,
207 particles,
208 global_best_position,
209 global_best_value,
210 rng: rng_gen,
211 nfev,
212 iteration: 0,
213 inertia_weight: options.w,
214 constriction_chi,
215 stagnation_counter: 0,
216 prev_global_best: global_best_value,
217 }
218 }
219
220 fn update_inertia_weight(&mut self) {
222 if self.options.adaptive {
223 let w_max = self.options.w;
225 let w_min = 0.4;
226 let progress = self.iteration as f64 / self.options.maxiter as f64;
227 self.inertia_weight = w_max - (w_max - w_min) * progress;
228 }
229 }
230
231 fn adaptive_coefficients(&self) -> (f64, f64) {
233 if !self.options.adaptive {
234 return (self.options.c1, self.options.c2);
235 }
236
237 let progress = self.iteration as f64 / self.options.maxiter as f64;
239 let c1 = 2.5 - 2.0 * progress;
240 let c2 = 0.5 + 2.0 * progress;
241 (c1, c2)
242 }
243
244 fn neighborhood_best(&self, particle_idx: usize) -> &Array1<f64> {
246 match self.options.topology {
247 Topology::Star => &self.global_best_position,
248 Topology::Ring => {
249 let n = self.particles.len();
250 let half_neighbors = self.options.ring_neighbors / 2;
251 let mut best_idx = particle_idx;
252 let mut best_val = self.particles[particle_idx].best_value;
253
254 for offset in 1..=half_neighbors {
255 let left = if particle_idx >= offset {
257 particle_idx - offset
258 } else {
259 n - (offset - particle_idx)
260 };
261 if self.particles[left].best_value < best_val {
262 best_val = self.particles[left].best_value;
263 best_idx = left;
264 }
265
266 let right = (particle_idx + offset) % n;
268 if self.particles[right].best_value < best_val {
269 best_val = self.particles[right].best_value;
270 best_idx = right;
271 }
272 }
273
274 &self.particles[best_idx].best_position
275 }
276 }
277 }
278
279 fn update_particle(&mut self, idx: usize) {
281 let (c1, c2) = self.adaptive_coefficients();
282 let nbest = self.neighborhood_best(idx).clone();
283
284 let particle = &mut self.particles[idx];
285
286 match self.options.velocity_strategy {
288 VelocityStrategy::InertiaWeight => {
289 for j in 0..self.ndim {
290 let r1 = self.rng.random_range(0.0..1.0);
291 let r2 = self.rng.random_range(0.0..1.0);
292
293 let cognitive = c1 * r1 * (particle.best_position[j] - particle.position[j]);
294 let social = c2 * r2 * (nbest[j] - particle.position[j]);
295
296 particle.velocity[j] =
297 self.inertia_weight * particle.velocity[j] + cognitive + social;
298 }
299 }
300 VelocityStrategy::Constriction => {
301 for j in 0..self.ndim {
302 let r1 = self.rng.random_range(0.0..1.0);
303 let r2 = self.rng.random_range(0.0..1.0);
304
305 let cognitive = c1 * r1 * (particle.best_position[j] - particle.position[j]);
306 let social = c2 * r2 * (nbest[j] - particle.position[j]);
307
308 particle.velocity[j] =
309 self.constriction_chi * (particle.velocity[j] + cognitive + social);
310 }
311 }
312 }
313
314 for j in 0..self.ndim {
316 let (lb, ub) = self.bounds[j];
317 let range = ub - lb;
318 let v_max = self.options.vmax * range;
319 let v_min = self.options.vmin * range;
320 particle.velocity[j] = particle.velocity[j].max(v_min).min(v_max);
321 }
322
323 for j in 0..self.ndim {
325 particle.position[j] += particle.velocity[j];
326
327 let (lb, ub) = self.bounds[j];
329 if particle.position[j] < lb {
330 particle.position[j] = lb + (lb - particle.position[j]).min(ub - lb);
331 particle.velocity[j] *= -0.5; } else if particle.position[j] > ub {
333 particle.position[j] = ub - (particle.position[j] - ub).min(ub - lb);
334 particle.velocity[j] *= -0.5; }
336
337 particle.position[j] = particle.position[j].max(lb).min(ub);
339 }
340
341 let value = (self.func)(&particle.position.view());
343 self.nfev += 1;
344
345 if value < particle.best_value {
347 particle.best_value = value;
348 particle.best_position = particle.position.clone();
349
350 if value < self.global_best_value {
352 self.global_best_value = value;
353 self.global_best_position = particle.position.clone();
354 }
355 }
356 }
357
358 fn check_convergence(&self) -> bool {
360 let mut max_distance: f64 = 0.0;
362
363 for particle in &self.particles {
364 let distance = (&particle.position - &self.global_best_position)
365 .mapv(|x| x.abs())
366 .sum();
367 max_distance = max_distance.max(distance);
368 }
369
370 max_distance < self.options.tol
371 }
372
373 fn handle_stagnation(&mut self) {
375 let improvement = self.prev_global_best - self.global_best_value;
377 if improvement.abs() < 1e-15 * self.global_best_value.abs().max(1.0) {
378 self.stagnation_counter += 1;
379 } else {
380 self.stagnation_counter = 0;
381 }
382 self.prev_global_best = self.global_best_value;
383
384 if self.stagnation_counter >= self.options.stagnation_limit {
386 self.stagnation_counter = 0;
387
388 let n_reinit =
389 (self.options.reinit_fraction * self.particles.len() as f64).ceil() as usize;
390 let n_reinit = n_reinit.max(1).min(self.particles.len());
391
392 let mut indices: Vec<usize> = (0..self.particles.len()).collect();
394 indices.sort_by(|&a, &b| {
395 self.particles[b]
396 .best_value
397 .partial_cmp(&self.particles[a].best_value)
398 .unwrap_or(std::cmp::Ordering::Equal)
399 });
400
401 for &idx in indices.iter().take(n_reinit) {
403 let particle = &mut self.particles[idx];
404 for j in 0..self.ndim {
405 let (lb, ub) = self.bounds[j];
406 particle.position[j] = self.rng.random_range(lb..ub);
407 particle.velocity[j] =
408 self.rng.random_range(self.options.vmin..self.options.vmax) * (ub - lb);
409 }
410 let value = (self.func)(&particle.position.view());
411 self.nfev += 1;
412 particle.best_position = particle.position.clone();
413 particle.best_value = value;
414
415 if value < self.global_best_value {
416 self.global_best_value = value;
417 self.global_best_position = particle.position.clone();
418 }
419 }
420 }
421 }
422
423 fn step(&mut self) -> bool {
425 self.iteration += 1;
426 self.update_inertia_weight();
427
428 for i in 0..self.options.swarm_size {
430 self.update_particle(i);
431 }
432
433 self.handle_stagnation();
435
436 self.check_convergence()
437 }
438
439 pub fn run(&mut self) -> OptimizeResult<f64> {
441 let mut converged = false;
442
443 for _ in 0..self.options.maxiter {
444 converged = self.step();
445
446 if converged {
447 break;
448 }
449 }
450
451 OptimizeResult {
452 x: self.global_best_position.clone(),
453 fun: self.global_best_value,
454 nfev: self.nfev,
455 func_evals: self.nfev,
456 nit: self.iteration,
457 success: converged,
458 message: if converged {
459 "Optimization converged successfully"
460 } else {
461 "Maximum number of iterations reached"
462 }
463 .to_string(),
464 ..Default::default()
465 }
466 }
467
468 pub fn best_value(&self) -> f64 {
470 self.global_best_value
471 }
472
473 pub fn best_position(&self) -> &Array1<f64> {
475 &self.global_best_position
476 }
477
478 pub fn function_evaluations(&self) -> usize {
480 self.nfev
481 }
482}
483
484#[allow(dead_code)]
496pub fn particle_swarm<F>(
497 func: F,
498 bounds: Bounds,
499 options: Option<ParticleSwarmOptions>,
500) -> Result<OptimizeResult<f64>, OptimizeError>
501where
502 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
503{
504 if bounds.is_empty() {
505 return Err(OptimizeError::InvalidInput(
506 "Bounds must not be empty".to_string(),
507 ));
508 }
509 for (i, &(lb, ub)) in bounds.iter().enumerate() {
510 if lb >= ub {
511 return Err(OptimizeError::InvalidInput(format!(
512 "Lower bound must be less than upper bound for dimension {} ({} >= {})",
513 i, lb, ub
514 )));
515 }
516 }
517
518 let options = options.unwrap_or_default();
519 if options.swarm_size == 0 {
520 return Err(OptimizeError::InvalidInput(
521 "Swarm size must be > 0".to_string(),
522 ));
523 }
524
525 let mut solver = ParticleSwarm::new(func, bounds, options);
526 Ok(solver.run())
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 fn sphere(x: &ArrayView1<f64>) -> f64 {
535 x.iter().map(|xi| xi * xi).sum()
536 }
537
538 fn rastrigin(x: &ArrayView1<f64>) -> f64 {
540 let n = x.len() as f64;
541 10.0 * n
542 + x.iter()
543 .map(|xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
544 .sum::<f64>()
545 }
546
547 fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
549 let mut sum = 0.0;
550 for i in 0..x.len() - 1 {
551 sum += 100.0 * (x[i + 1] - x[i].powi(2)).powi(2) + (1.0 - x[i]).powi(2);
552 }
553 sum
554 }
555
556 #[test]
557 fn test_pso_sphere_2d() {
558 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
559 let mut opts = ParticleSwarmOptions::default();
560 opts.seed = Some(42);
561 opts.swarm_size = 30;
562 opts.maxiter = 200;
563
564 let result = particle_swarm(sphere, bounds, Some(opts));
565 assert!(result.is_ok());
566 let res = result.expect("should succeed");
567 assert!(
568 res.fun < 0.01,
569 "Sphere function should be near 0, got {}",
570 res.fun
571 );
572 }
573
574 #[test]
575 fn test_pso_sphere_adaptive() {
576 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
577 let mut opts = ParticleSwarmOptions::default();
578 opts.seed = Some(123);
579 opts.swarm_size = 40;
580 opts.maxiter = 300;
581 opts.adaptive = true;
582
583 let result = particle_swarm(sphere, bounds, Some(opts));
584 assert!(result.is_ok());
585 let res = result.expect("should succeed");
586 assert!(
587 res.fun < 0.01,
588 "Adaptive PSO should find sphere min, got {}",
589 res.fun
590 );
591 }
592
593 #[test]
594 fn test_pso_constriction_coefficient() {
595 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
596 let mut opts = ParticleSwarmOptions::default();
597 opts.seed = Some(99);
598 opts.swarm_size = 40;
599 opts.maxiter = 300;
600 opts.velocity_strategy = VelocityStrategy::Constriction;
601
602 let result = particle_swarm(sphere, bounds, Some(opts));
603 assert!(result.is_ok());
604 let res = result.expect("should succeed");
605 assert!(
606 res.fun < 0.1,
607 "Constriction PSO should find near-optimal, got {}",
608 res.fun
609 );
610 }
611
612 #[test]
613 fn test_pso_ring_topology() {
614 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
615 let mut opts = ParticleSwarmOptions::default();
616 opts.seed = Some(55);
617 opts.swarm_size = 30;
618 opts.maxiter = 300;
619 opts.topology = Topology::Ring;
620 opts.ring_neighbors = 5;
621
622 let result = particle_swarm(sphere, bounds, Some(opts));
623 assert!(result.is_ok());
624 let res = result.expect("should succeed");
625 assert!(
626 res.fun < 0.1,
627 "Ring topology should find near-optimal, got {}",
628 res.fun
629 );
630 }
631
632 #[test]
633 fn test_pso_rastrigin() {
634 let bounds = vec![(-5.12, 5.12), (-5.12, 5.12)];
636 let mut opts = ParticleSwarmOptions::default();
637 opts.seed = Some(42);
638 opts.swarm_size = 60;
639 opts.maxiter = 500;
640 opts.adaptive = true;
641
642 let result = particle_swarm(rastrigin, bounds, Some(opts));
643 assert!(result.is_ok());
644 let res = result.expect("should succeed");
645 assert!(
647 res.fun < 5.0,
648 "PSO should find reasonable Rastrigin value, got {}",
649 res.fun
650 );
651 }
652
653 #[test]
654 fn test_pso_rosenbrock() {
655 let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
656 let mut opts = ParticleSwarmOptions::default();
657 opts.seed = Some(42);
658 opts.swarm_size = 50;
659 opts.maxiter = 500;
660 opts.adaptive = true;
661
662 let result = particle_swarm(rosenbrock, bounds, Some(opts));
663 assert!(result.is_ok());
664 let res = result.expect("should succeed");
665 assert!(
667 res.fun < 5.0,
668 "PSO should find reasonable Rosenbrock value, got {}",
669 res.fun
670 );
671 }
672
673 #[test]
674 fn test_pso_invalid_bounds() {
675 let result = particle_swarm(sphere, vec![], None);
677 assert!(result.is_err());
678
679 let bounds = vec![(5.0, 2.0)];
681 let result = particle_swarm(sphere, bounds, None);
682 assert!(result.is_err());
683 }
684
685 #[test]
686 fn test_pso_invalid_swarm_size() {
687 let bounds = vec![(-5.0, 5.0)];
688 let mut opts = ParticleSwarmOptions::default();
689 opts.swarm_size = 0;
690 let result = particle_swarm(sphere, bounds, Some(opts));
691 assert!(result.is_err());
692 }
693
694 #[test]
695 fn test_pso_convergence_detection() {
696 let bounds = vec![(-1.0, 1.0)];
698 let mut opts = ParticleSwarmOptions::default();
699 opts.seed = Some(42);
700 opts.swarm_size = 20;
701 opts.maxiter = 1000;
702 opts.tol = 1e-3; let func = |x: &ArrayView1<f64>| -> f64 { x[0] * x[0] };
705 let result = particle_swarm(func, bounds, Some(opts));
706 assert!(result.is_ok());
707 let res = result.expect("should succeed");
708 assert!(res.nit > 0);
709 assert!(res.nfev > 0);
710 }
711
712 #[test]
713 fn test_pso_stagnation_restart() {
714 let bounds = vec![(-10.0, 10.0), (-10.0, 10.0)];
716 let mut opts = ParticleSwarmOptions::default();
717 opts.seed = Some(42);
718 opts.swarm_size = 20;
719 opts.maxiter = 100;
720 opts.stagnation_limit = 5;
721 opts.reinit_fraction = 0.3;
722
723 let result = particle_swarm(sphere, bounds, Some(opts));
724 assert!(result.is_ok());
725 let res = result.expect("should succeed");
726 assert!(res.fun < 1.0);
728 }
729
730 #[test]
731 fn test_pso_high_dimensional() {
732 let n = 10;
733 let bounds: Vec<(f64, f64)> = vec![(-5.0, 5.0); n];
734 let mut opts = ParticleSwarmOptions::default();
735 opts.seed = Some(42);
736 opts.swarm_size = 100;
737 opts.maxiter = 500;
738 opts.adaptive = true;
739
740 let result = particle_swarm(sphere, bounds, Some(opts));
741 assert!(result.is_ok());
742 let res = result.expect("should succeed");
743 assert!(
744 res.fun < 1.0,
745 "High-dim PSO should converge reasonably, got {}",
746 res.fun
747 );
748 }
749
750 #[test]
751 fn test_pso_options_default() {
752 let opts = ParticleSwarmOptions::default();
753 assert_eq!(opts.swarm_size, 50);
754 assert_eq!(opts.maxiter, 500);
755 assert!((opts.c1 - 2.0).abs() < 1e-10);
756 assert!((opts.c2 - 2.0).abs() < 1e-10);
757 assert!((opts.w - 0.9).abs() < 1e-10);
758 assert_eq!(opts.topology, Topology::Star);
759 assert_eq!(opts.velocity_strategy, VelocityStrategy::InertiaWeight);
760 }
761
762 #[test]
763 fn test_pso_solver_accessors() {
764 let bounds = vec![(-5.0, 5.0)];
765 let mut opts = ParticleSwarmOptions::default();
766 opts.seed = Some(42);
767 opts.swarm_size = 10;
768
769 let solver = ParticleSwarm::new(sphere, bounds, opts);
770 assert!(solver.best_value().is_finite());
771 assert_eq!(solver.best_position().len(), 1);
772 assert!(solver.function_evaluations() > 0);
773 }
774}