scirs2_optimize/global/
particle_swarm.rs

1//! Particle Swarm Optimization (PSO) algorithm for global optimization
2//!
3//! PSO is a population-based stochastic optimization algorithm inspired by
4//! the social behavior of bird flocking or fish schooling. Each particle
5//! moves through the search space with a velocity influenced by its own
6//! best position and the global best position found by the swarm.
7
8use crate::error::OptimizeError;
9use crate::unconstrained::OptimizeResult;
10use ndarray::{Array1, ArrayView1};
11use rand::prelude::*;
12use rand::rngs::StdRng;
13
14/// Options for Particle Swarm Optimization
15#[derive(Debug, Clone)]
16pub struct ParticleSwarmOptions {
17    /// Number of particles in the swarm
18    pub swarm_size: usize,
19    /// Maximum number of iterations
20    pub maxiter: usize,
21    /// Cognitive parameter (attraction to personal best)
22    pub c1: f64,
23    /// Social parameter (attraction to global best)
24    pub c2: f64,
25    /// Inertia weight
26    pub w: f64,
27    /// Minimum velocity
28    pub vmin: f64,
29    /// Maximum velocity
30    pub vmax: f64,
31    /// Tolerance for convergence
32    pub tol: f64,
33    /// Random seed for reproducibility
34    pub seed: Option<u64>,
35    /// Whether to use adaptive parameters
36    pub adaptive: bool,
37}
38
39impl Default for ParticleSwarmOptions {
40    fn default() -> Self {
41        Self {
42            swarm_size: 50,
43            maxiter: 500,
44            c1: 2.0,
45            c2: 2.0,
46            w: 0.9,
47            vmin: -0.5,
48            vmax: 0.5,
49            tol: 1e-8,
50            seed: None,
51            adaptive: false,
52        }
53    }
54}
55
56/// Bounds for variables
57pub type Bounds = Vec<(f64, f64)>;
58
59/// Particle in the swarm
60#[derive(Debug, Clone)]
61struct Particle {
62    /// Current position
63    position: Array1<f64>,
64    /// Current velocity
65    velocity: Array1<f64>,
66    /// Personal best position
67    best_position: Array1<f64>,
68    /// Personal best value
69    best_value: f64,
70}
71
72/// Particle Swarm Optimization solver
73pub struct ParticleSwarm<F>
74where
75    F: Fn(&ArrayView1<f64>) -> f64 + Clone,
76{
77    func: F,
78    bounds: Bounds,
79    options: ParticleSwarmOptions,
80    ndim: usize,
81    particles: Vec<Particle>,
82    global_best_position: Array1<f64>,
83    global_best_value: f64,
84    rng: StdRng,
85    nfev: usize,
86    iteration: usize,
87    inertia_weight: f64,
88}
89
90impl<F> ParticleSwarm<F>
91where
92    F: Fn(&ArrayView1<f64>) -> f64 + Clone,
93{
94    /// Create new Particle Swarm Optimization solver
95    pub fn new(func: F, bounds: Bounds, options: ParticleSwarmOptions) -> Self {
96        let ndim = bounds.len();
97        let seed = options.seed.unwrap_or_else(rand::random);
98        let mut rng = StdRng::seed_from_u64(seed);
99
100        // Initialize particles
101        let mut particles = Vec::with_capacity(options.swarm_size);
102        let mut global_best_position = Array1::zeros(ndim);
103        let mut global_best_value = f64::INFINITY;
104        let mut nfev = 0;
105
106        for _ in 0..options.swarm_size {
107            // Random initial position within bounds
108            let mut position = Array1::zeros(ndim);
109            let mut velocity = Array1::zeros(ndim);
110
111            for j in 0..ndim {
112                let (lb, ub) = bounds[j];
113                position[j] = rng.random_range(lb..ub);
114                velocity[j] = rng.random_range(options.vmin..options.vmax) * (ub - lb);
115            }
116
117            // Evaluate initial position
118            let value = func(&position.view());
119            nfev += 1;
120
121            // Update global best if necessary
122            if value < global_best_value {
123                global_best_value = value;
124                global_best_position = position.clone();
125            }
126
127            particles.push(Particle {
128                position: position.clone(),
129                velocity,
130                best_position: position,
131                best_value: value,
132            });
133        }
134
135        Self {
136            func,
137            bounds,
138            options: options.clone(),
139            ndim,
140            particles,
141            global_best_position,
142            global_best_value,
143            rng,
144            nfev,
145            iteration: 0,
146            inertia_weight: options.w,
147        }
148    }
149
150    /// Update the inertia weight adaptively
151    fn update_inertia_weight(&mut self) {
152        if self.options.adaptive {
153            // Linear decrease from w to 0.4
154            let w_max = self.options.w;
155            let w_min = 0.4;
156            self.inertia_weight =
157                w_max - (w_max - w_min) * (self.iteration as f64 / self.options.maxiter as f64);
158        }
159    }
160
161    /// Update particle velocity and position
162    fn update_particle(&mut self, idx: usize) {
163        let particle = &mut self.particles[idx];
164
165        // Update velocity
166        for j in 0..self.ndim {
167            let r1 = self.rng.random::<f64>();
168            let r2 = self.rng.random::<f64>();
169
170            // Velocity update formula
171            let cognitive =
172                self.options.c1 * r1 * (particle.best_position[j] - particle.position[j]);
173            let social =
174                self.options.c2 * r2 * (self.global_best_position[j] - particle.position[j]);
175
176            particle.velocity[j] = self.inertia_weight * particle.velocity[j] + cognitive + social;
177
178            // Clamp velocity
179            let (lb, ub) = self.bounds[j];
180            let vmax = self.options.vmax * (ub - lb);
181            let vmin = self.options.vmin * (ub - lb);
182            particle.velocity[j] = particle.velocity[j].max(vmin).min(vmax);
183        }
184
185        // Update position
186        for j in 0..self.ndim {
187            particle.position[j] += particle.velocity[j];
188
189            // Enforce bounds
190            let (lb, ub) = self.bounds[j];
191            if particle.position[j] < lb {
192                particle.position[j] = lb;
193                particle.velocity[j] = 0.0; // Reset velocity at boundary
194            } else if particle.position[j] > ub {
195                particle.position[j] = ub;
196                particle.velocity[j] = 0.0; // Reset velocity at boundary
197            }
198        }
199
200        // Evaluate new position
201        let value = (self.func)(&particle.position.view());
202        self.nfev += 1;
203
204        // Update personal best
205        if value < particle.best_value {
206            particle.best_value = value;
207            particle.best_position = particle.position.clone();
208
209            // Update global best
210            if value < self.global_best_value {
211                self.global_best_value = value;
212                self.global_best_position = particle.position.clone();
213            }
214        }
215    }
216
217    /// Check convergence criterion
218    fn check_convergence(&self) -> bool {
219        // Check if all particles have converged to the same region
220        let mut max_distance: f64 = 0.0;
221
222        for particle in &self.particles {
223            let distance = (&particle.position - &self.global_best_position)
224                .mapv(|x| x.abs())
225                .sum();
226            max_distance = max_distance.max(distance);
227        }
228
229        max_distance < self.options.tol
230    }
231
232    /// Run one iteration of the algorithm
233    fn step(&mut self) -> bool {
234        self.iteration += 1;
235        self.update_inertia_weight();
236
237        // Update all particles
238        for i in 0..self.options.swarm_size {
239            self.update_particle(i);
240        }
241
242        self.check_convergence()
243    }
244
245    /// Run the particle swarm optimization algorithm
246    pub fn run(&mut self) -> OptimizeResult<f64> {
247        let mut converged = false;
248
249        for _ in 0..self.options.maxiter {
250            converged = self.step();
251
252            if converged {
253                break;
254            }
255        }
256
257        OptimizeResult {
258            x: self.global_best_position.clone(),
259            fun: self.global_best_value,
260            nfev: self.nfev,
261            func_evals: self.nfev,
262            nit: self.iteration,
263            iterations: self.iteration,
264            success: converged,
265            message: if converged {
266                "Optimization converged successfully"
267            } else {
268                "Maximum number of iterations reached"
269            }
270            .to_string(),
271            ..Default::default()
272        }
273    }
274}
275
276/// Perform global optimization using particle swarm optimization
277pub fn particle_swarm<F>(
278    func: F,
279    bounds: Bounds,
280    options: Option<ParticleSwarmOptions>,
281) -> Result<OptimizeResult<f64>, OptimizeError>
282where
283    F: Fn(&ArrayView1<f64>) -> f64 + Clone,
284{
285    let options = options.unwrap_or_default();
286    let mut solver = ParticleSwarm::new(func, bounds, options);
287    Ok(solver.run())
288}