scirs2_optimize/global/
particle_swarm.rs1use crate::error::OptimizeError;
9use crate::unconstrained::OptimizeResult;
10use ndarray::{Array1, ArrayView1};
11use rand::prelude::*;
12use rand::rngs::StdRng;
13
14#[derive(Debug, Clone)]
16pub struct ParticleSwarmOptions {
17 pub swarm_size: usize,
19 pub maxiter: usize,
21 pub c1: f64,
23 pub c2: f64,
25 pub w: f64,
27 pub vmin: f64,
29 pub vmax: f64,
31 pub tol: f64,
33 pub seed: Option<u64>,
35 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
56pub type Bounds = Vec<(f64, f64)>;
58
59#[derive(Debug, Clone)]
61struct Particle {
62 position: Array1<f64>,
64 velocity: Array1<f64>,
66 best_position: Array1<f64>,
68 best_value: f64,
70}
71
72pub 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 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 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 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 let value = func(&position.view());
119 nfev += 1;
120
121 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 fn update_inertia_weight(&mut self) {
152 if self.options.adaptive {
153 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 fn update_particle(&mut self, idx: usize) {
163 let particle = &mut self.particles[idx];
164
165 for j in 0..self.ndim {
167 let r1 = self.rng.random::<f64>();
168 let r2 = self.rng.random::<f64>();
169
170 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 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 for j in 0..self.ndim {
187 particle.position[j] += particle.velocity[j];
188
189 let (lb, ub) = self.bounds[j];
191 if particle.position[j] < lb {
192 particle.position[j] = lb;
193 particle.velocity[j] = 0.0; } else if particle.position[j] > ub {
195 particle.position[j] = ub;
196 particle.velocity[j] = 0.0; }
198 }
199
200 let value = (self.func)(&particle.position.view());
202 self.nfev += 1;
203
204 if value < particle.best_value {
206 particle.best_value = value;
207 particle.best_position = particle.position.clone();
208
209 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 fn check_convergence(&self) -> bool {
219 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 fn step(&mut self) -> bool {
234 self.iteration += 1;
235 self.update_inertia_weight();
236
237 for i in 0..self.options.swarm_size {
239 self.update_particle(i);
240 }
241
242 self.check_convergence()
243 }
244
245 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
276pub 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}