non_convex_opt/algorithms/multi_swarm/
swarm.rs

1use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, Dyn, OMatrix, OVector, U1};
2use rand::{rngs::StdRng, Rng, SeedableRng};
3use rayon::prelude::*;
4
5use crate::utils::config::MSPOConf;
6use crate::utils::opt_prob::{FloatNumber as FloatNum, OptProb};
7
8use crate::algorithms::multi_swarm::particle::Particle;
9
10#[derive(Clone)]
11pub struct SwarmConfig<'a, T, D>
12where
13    T: FloatNum,
14    D: Dim,
15    DefaultAllocator: Allocator<D> + Allocator<U1, D> + Allocator<Dyn, D>,
16{
17    pub num_particles: usize,
18    pub dim: usize,
19    pub c1: T,
20    pub c2: T,
21    pub bounds: (T, T),
22    pub opt_prob: &'a OptProb<T, D>,
23    pub init_pop: OMatrix<T, Dyn, D>,
24    pub inertia_start: T,
25    pub inertia_end: T,
26    pub max_iterations: usize,
27}
28
29pub struct Swarm<T, D>
30where
31    T: FloatNum + Send + Sync,
32    D: Dim + Send + Sync,
33    DefaultAllocator: Allocator<D> + Allocator<U1, D>,
34{
35    pub particles: Vec<Particle<T, D>>,
36    pub global_best_position: OVector<T, D>,
37    pub global_best_fitness: T,
38    pub c1: T,
39    pub c2: T,
40    pub x_min: f64,
41    pub x_max: f64,
42    pub iteration_count: usize,
43    pub improvement_history: Vec<T>,
44    pub diversity_history: Vec<f64>,
45    pub inertia_start: T,
46    pub inertia_end: T,
47    pub max_iterations: usize,
48}
49
50impl<T, D> Swarm<T, D>
51where
52    T: FloatNum + Send + Sync,
53    D: Dim + Send + Sync,
54    OVector<T, D>: Send + Sync,
55    OMatrix<T, Dyn, D>: Send + Sync,
56    DefaultAllocator: Allocator<D> + Allocator<U1, D> + Allocator<Dyn, D>,
57{
58    pub fn new(config: SwarmConfig<T, D>, seed: u64) -> Self {
59        let _base_rng = StdRng::seed_from_u64(seed);
60        let particles: Vec<_> = (0..config.num_particles)
61            .into_par_iter()
62            .map_init(
63                || {
64                    let thread_id = rayon::current_thread_index().unwrap_or(0);
65                    StdRng::seed_from_u64(seed + thread_id as u64)
66                },
67                |rng, i| {
68                    let mut position =
69                        OVector::<T, D>::zeros_generic(D::from_usize(config.dim), U1);
70                    let fitness;
71
72                    if i < config.init_pop.nrows() {
73                        position = config.init_pop.row(i).transpose();
74                        fitness = config.opt_prob.evaluate(&position);
75                    } else {
76                        loop {
77                            let values = (0..config.dim).map(|_| {
78                                let r = T::from_f64(rng.random::<f64>()).unwrap();
79                                config.bounds.0 + (config.bounds.1 - config.bounds.0) * r
80                            });
81                            let position: OVector<T, D> = OVector::from_iterator_generic(
82                                D::from_usize(config.dim),
83                                U1,
84                                values,
85                            );
86
87                            if config.opt_prob.is_feasible(&position) {
88                                fitness = config.opt_prob.evaluate(&position);
89                                break;
90                            }
91                        }
92                    }
93
94                    let values = (0..config.dim).map(|_| {
95                        let r = T::from_f64(rng.random::<f64>()).unwrap();
96                        (config.bounds.1 - config.bounds.0)
97                            * (r - T::from_f64(0.5).unwrap())
98                            * T::from_f64(0.1).unwrap()
99                    });
100
101                    let velocity: OVector<T, D> =
102                        OVector::from_iterator_generic(D::from_usize(config.dim), U1, values);
103
104                    Particle::new(position, velocity, fitness, rng.clone())
105                },
106            )
107            .collect();
108
109        let mut best_fitness = T::neg_infinity();
110        let mut best_position = OVector::<T, D>::zeros_generic(D::from_usize(config.dim), U1);
111
112        for particle in &particles {
113            if particle.best_fitness > best_fitness {
114                best_fitness = particle.best_fitness;
115                best_position = particle.position.clone();
116            }
117        }
118
119        Self {
120            particles,
121            global_best_position: best_position,
122            global_best_fitness: best_fitness,
123            c1: config.c1,
124            c2: config.c2,
125            x_min: config.bounds.0.to_f64().unwrap(),
126            x_max: config.bounds.1.to_f64().unwrap(),
127            iteration_count: 0,
128            improvement_history: Vec::new(),
129            diversity_history: Vec::new(),
130            inertia_start: config.inertia_start,
131            inertia_end: config.inertia_end,
132            max_iterations: config.max_iterations,
133        }
134    }
135
136    pub fn update(&mut self, opt_prob: &OptProb<T, D>) {
137        let bounds = (
138            T::from_f64(self.x_min).unwrap(),
139            T::from_f64(self.x_max).unwrap(),
140        );
141
142        // Adaptive weight based on stagnation and diversity
143        let adaptive_w = self.compute_adaptive_inertia();
144
145        self.particles.par_iter_mut().for_each(|particle| {
146            particle.update_velocity_and_position(
147                &self.global_best_position,
148                adaptive_w,
149                self.c1,
150                self.c2,
151                opt_prob,
152                bounds,
153            );
154        });
155
156        let best_particle = self
157            .particles
158            .par_iter()
159            .reduce_with(|p1, p2| {
160                if p1.best_fitness > p2.best_fitness {
161                    p1
162                } else {
163                    p2
164                }
165            })
166            .unwrap();
167
168        if best_particle.best_fitness > self.global_best_fitness {
169            let improvement = best_particle.best_fitness - self.global_best_fitness;
170            self.improvement_history.push(improvement);
171
172            self.global_best_fitness = best_particle.best_fitness;
173            self.global_best_position = best_particle.best_position.clone();
174        }
175
176        let diversity = self.compute_diversity();
177        self.diversity_history.push(diversity);
178
179        self.iteration_count += 1;
180    }
181
182    // Linearly decrease from w_start to w_end over total run
183    fn compute_adaptive_inertia(&self) -> T {
184        let progress = (self.iteration_count as f64 / self.max_iterations as f64).min(1.0);
185
186        self.inertia_start
187            + (self.inertia_end - self.inertia_start) * T::from_f64(progress).unwrap()
188    }
189
190    // Avg distance between particles
191    fn compute_diversity(&self) -> f64 {
192        if self.particles.len() < 2 {
193            return 0.0;
194        }
195
196        let pairs: Vec<_> = (0..self.particles.len())
197            .flat_map(|i| (i + 1..self.particles.len()).map(move |j| (i, j)))
198            .collect();
199
200        if pairs.is_empty() {
201            return 0.0;
202        }
203
204        let total_distance: f64 = pairs
205            .par_iter()
206            .map(|&(i, j)| {
207                let distance = self
208                    .euclidean_distance(&self.particles[i].position, &self.particles[j].position);
209                distance.to_f64().unwrap()
210            })
211            .sum();
212
213        total_distance / pairs.len() as f64
214    }
215
216    fn euclidean_distance(&self, v1: &OVector<T, D>, v2: &OVector<T, D>) -> T {
217        let diff = v1 - v2;
218        diff.dot(&diff).sqrt()
219    }
220
221    pub fn is_stagnated(&self, threshold: usize) -> bool {
222        if self.improvement_history.len() < threshold {
223            return false;
224        }
225
226        let recent_improvements: Vec<T> = self
227            .improvement_history
228            .iter()
229            .rev()
230            .take(threshold)
231            .cloned()
232            .collect();
233
234        let max_improvement = recent_improvements
235            .iter()
236            .fold(T::neg_infinity(), |a, &b| a.max(b));
237        max_improvement < T::epsilon()
238    }
239
240    pub fn average_improvement(&self, window_size: usize) -> T {
241        if self.improvement_history.is_empty() {
242            return T::zero();
243        }
244
245        let recent_improvements: Vec<T> = self
246            .improvement_history
247            .iter()
248            .rev()
249            .take(window_size.min(self.improvement_history.len()))
250            .cloned()
251            .collect();
252
253        let len = recent_improvements.len();
254        let sum = recent_improvements
255            .into_iter()
256            .fold(T::zero(), |acc, x| acc + x);
257        sum / T::from_usize(len).unwrap()
258    }
259
260    pub fn current_diversity(&self) -> f64 {
261        self.diversity_history.last().copied().unwrap_or(0.0)
262    }
263}
264
265pub fn initialize_swarms<T, N, D>(
266    conf: &MSPOConf,
267    dim: usize,
268    init_pop: &OMatrix<T, N, D>,
269    opt_prob: &OptProb<T, D>,
270    max_iter: usize,
271    seed: u64,
272) -> Vec<Swarm<T, D>>
273where
274    T: FloatNum,
275    N: Dim,
276    D: Dim,
277    OVector<T, D>: Send + Sync,
278    OMatrix<T, N, D>: Send + Sync,
279    DefaultAllocator: Allocator<D> + Allocator<N, D> + Allocator<U1, D> + Allocator<D, D>,
280{
281    let particles_per_swarm = conf.swarm_size;
282    let pop_per_swarm = init_pop.nrows() / conf.num_swarms;
283    let mut promising_centers: Vec<OVector<T, D>> = Vec::new();
284
285    let fitness_values: Vec<_> = (0..init_pop.nrows())
286        .into_par_iter()
287        .map(|i| {
288            let individual = init_pop.row(i).transpose();
289            (i, opt_prob.evaluate(&individual))
290        })
291        .collect();
292
293    let mut sorted_indices: Vec<usize> = (0..init_pop.nrows()).collect();
294    sorted_indices.sort_by(|&i, &j| {
295        fitness_values[i]
296            .1
297            .partial_cmp(&fitness_values[j].1)
298            .unwrap()
299    });
300
301    // Find diverse centers with min dist constraint
302    for &idx in sorted_indices.iter().take(conf.num_swarms) {
303        let center = init_pop.row(idx).transpose();
304
305        // Centers should be far apart (for diversity)
306        let min_distance = T::from_f64(0.3 * (conf.x_max - conf.x_min)).unwrap();
307
308        if promising_centers.is_empty() {
309            promising_centers.push(center);
310        } else {
311            let is_diverse = promising_centers.par_iter().all(|c| {
312                let dist = (c - &center).dot(&(c - &center)).sqrt();
313                dist > min_distance
314            });
315
316            if is_diverse {
317                promising_centers.push(center);
318            }
319        }
320    }
321
322    let mut fill_rng = StdRng::seed_from_u64(seed + 1000);
323    while promising_centers.len() < conf.num_swarms {
324        let random_center = OVector::<T, D>::from_iterator_generic(
325            D::from_usize(dim),
326            U1,
327            (0..dim).map(|_| {
328                T::from_f64(conf.x_min + fill_rng.random::<f64>() * (conf.x_max - conf.x_min))
329                    .unwrap()
330            }),
331        );
332        promising_centers.push(random_center);
333    }
334
335    // Fill with promising, fallback to random
336    let _base_rng = StdRng::seed_from_u64(seed + 2000);
337    (0..conf.num_swarms)
338        .into_par_iter()
339        .map_init(
340            || {
341                let thread_id = rayon::current_thread_index().unwrap_or(0);
342                StdRng::seed_from_u64(seed + 2000 + thread_id as u64)
343            },
344            |rng, i| {
345                let center = if i < promising_centers.len() {
346                    promising_centers[i].clone()
347                } else {
348                    OVector::<T, D>::from_iterator_generic(
349                        D::from_usize(dim),
350                        U1,
351                        (0..dim).map(|_| {
352                            T::from_f64(
353                                conf.x_min + rng.random::<f64>() * (conf.x_max - conf.x_min),
354                            )
355                            .unwrap()
356                        }),
357                    )
358                };
359
360                // Spawn particles around center with controlled spread
361                let radius = T::from_f64(0.15 * (conf.x_max - conf.x_min)).unwrap(); // Smaller radius for better convergence
362                let start_idx = i * pop_per_swarm;
363                let mut swarm_pop: OMatrix<T, Dyn, D> =
364                    init_pop.rows(start_idx, particles_per_swarm).into_owned();
365
366                let particle_adjustments: Vec<_> = (0..particles_per_swarm)
367                    .into_par_iter()
368                    .map_init(
369                        || {
370                            let thread_id = rayon::current_thread_index().unwrap_or(0);
371                            StdRng::seed_from_u64(seed + 3000 + thread_id as u64)
372                        },
373                        |particle_rng, j| {
374                            let mut particle_row = Vec::with_capacity(dim);
375                            for k in 0..dim {
376                                let r = T::from_f64(particle_rng.random::<f64>()).unwrap();
377                                let noise = (r - T::from_f64(0.5).unwrap()) * radius;
378                                let adjusted_value = (center[k] + noise).clamp(
379                                    T::from_f64(conf.x_min).unwrap(),
380                                    T::from_f64(conf.x_max).unwrap(),
381                                );
382                                particle_row.push(adjusted_value);
383                            }
384                            (j, particle_row)
385                        },
386                    )
387                    .collect();
388
389                for (j, particle_row) in particle_adjustments {
390                    for (k, &value) in particle_row.iter().enumerate() {
391                        swarm_pop[(j, k)] = value;
392                    }
393                }
394
395                Swarm::new(
396                    SwarmConfig {
397                        num_particles: particles_per_swarm,
398                        dim,
399                        c1: T::from_f64(conf.c1).unwrap(),
400                        c2: T::from_f64(conf.c2).unwrap(),
401                        bounds: (
402                            T::from_f64(conf.x_min).unwrap(),
403                            T::from_f64(conf.x_max).unwrap(),
404                        ),
405                        opt_prob,
406                        init_pop: swarm_pop,
407                        inertia_start: T::from_f64(conf.inertia_start).unwrap(),
408                        inertia_end: T::from_f64(conf.inertia_end).unwrap(),
409                        max_iterations: max_iter,
410                    },
411                    seed,
412                )
413            },
414        )
415        .collect()
416}