metaheurustics/algorithm/
de.rs

1use ndarray::{Array1, Array2};
2use rand::Rng;
3use rand::seq::SliceRandom;
4use crate::{MetaheuristicError, Result};
5use super::{ObjectiveFunction, OptimizationParams, OptimizationResult, argmin};
6
7/// Different DE mutation strategies
8#[derive(Debug, Clone, Copy)]
9pub enum DEStrategy {
10    RandOneBin,
11    BestOneBin,
12    RandTwoBin,
13}
14
15/// Parameters specific to the Differential Evolution algorithm
16#[derive(Debug, Clone)]
17pub struct DEParams {
18    /// General optimization parameters
19    pub opt_params: OptimizationParams,
20    /// Mutation strategy
21    pub strategy: DEStrategy,
22    /// Crossover rate
23    pub crossover_rate: f64,
24    /// Mutation factor (F)
25    pub mutation_factor: f64,
26}
27
28impl Default for DEParams {
29    fn default() -> Self {
30        Self {
31            opt_params: OptimizationParams::default(),
32            strategy: DEStrategy::RandOneBin,
33            crossover_rate: 0.9,
34            mutation_factor: 0.8,
35        }
36    }
37}
38
39/// Implementation of the Differential Evolution algorithm
40pub struct DE<'a> {
41    objective: &'a dyn ObjectiveFunction,
42    params: DEParams,
43}
44
45impl<'a> DE<'a> {
46    /// Create a new instance of DE
47    pub fn new(objective: &'a dyn ObjectiveFunction, params: DEParams) -> Self {
48        Self { objective, params }
49    }
50    
51    /// Run the optimization algorithm
52    pub fn optimize(&self) -> Result<OptimizationResult> {
53        let mut rng = rand::thread_rng();
54        
55        // Initialize population
56        let mut population = self.initialize_population(&mut rng)?;
57        let mut fitness = self.evaluate_population(&population);
58        
59        let best_idx = argmin(&fitness).expect("Failed to find minimum fitness");
60        let mut best_solution = population.row(best_idx).to_owned();
61        let mut best_fitness = fitness[best_idx];
62        
63        let mut evaluations = self.params.opt_params.population_size;
64        
65        for _iteration in 0..self.params.opt_params.max_iterations {
66            // Create new population through mutation and crossover
67            let mut new_population = Array2::zeros(population.raw_dim());
68            
69            for i in 0..self.params.opt_params.population_size {
70                let trial_vector = match self.params.strategy {
71                    DEStrategy::RandOneBin => self.rand_1_bin(i, &population, &mut rng),
72                    DEStrategy::BestOneBin => self.best_1_bin(i, &population, best_idx, &mut rng),
73                    DEStrategy::RandTwoBin => self.rand_2_bin(i, &population, &mut rng),
74                };
75                
76                // Evaluate trial vector
77                let trial_fitness = self.objective.evaluate(&trial_vector);
78                evaluations += 1;
79                
80                // Selection
81                if trial_fitness <= fitness[i] {
82                    new_population.row_mut(i).assign(&trial_vector);
83                    fitness[i] = trial_fitness;
84                    
85                    // Update best solution
86                    if trial_fitness < best_fitness {
87                        best_fitness = trial_fitness;
88                        best_solution = trial_vector.clone();
89                    }
90                } else {
91                    new_population.row_mut(i).assign(&population.row(i));
92                }
93            }
94            
95            population = new_population;
96            
97            // Check termination criteria
98            if let Some(target) = self.params.opt_params.target_value {
99                if best_fitness <= target {
100                    break;
101                }
102            }
103        }
104        
105        Ok(OptimizationResult {
106            best_solution,
107            best_fitness,
108            iterations: self.params.opt_params.max_iterations,
109            evaluations,
110        })
111    }
112    
113    /// Initialize the population
114    fn initialize_population(&self, rng: &mut impl Rng) -> Result<Array2<f64>> {
115        let (lower_bounds, upper_bounds) = self.objective.bounds();
116        let dims = self.objective.dimensions();
117        
118        if lower_bounds.len() != dims || upper_bounds.len() != dims {
119            return Err(MetaheuristicError::InvalidDimension {
120                expected: dims,
121                got: lower_bounds.len(),
122            });
123        }
124        
125        let mut population = Array2::zeros((self.params.opt_params.population_size, dims));
126        for i in 0..self.params.opt_params.population_size {
127            for j in 0..dims {
128                population[[i, j]] = rng.gen_range(lower_bounds[j]..=upper_bounds[j]);
129            }
130        }
131        
132        Ok(population)
133    }
134    
135    /// Evaluate the entire population
136    fn evaluate_population(&self, population: &Array2<f64>) -> Array1<f64> {
137        let mut fitness = Array1::zeros(population.nrows());
138        for (i, row) in population.rows().into_iter().enumerate() {
139            fitness[i] = self.objective.evaluate(&row.to_owned());
140        }
141        fitness
142    }
143    
144    /// DE/rand/1/bin strategy
145    fn rand_1_bin(&self, target_idx: usize, population: &Array2<f64>, rng: &mut impl Rng) -> Array1<f64> {
146        let dims = self.objective.dimensions();
147        let (lower_bounds, upper_bounds) = self.objective.bounds();
148        
149        // Select random indices different from target
150        let mut indices: Vec<usize> = (0..self.params.opt_params.population_size)
151            .filter(|&x| x != target_idx)
152            .collect();
153        indices.shuffle(rng);
154        let (r1, r2, r3) = (indices[0], indices[1], indices[2]);
155        
156        // Create trial vector
157        let mut trial = population.row(target_idx).to_owned();
158        let j_rand = rng.gen_range(0..dims);
159        
160        for j in 0..dims {
161            if j == j_rand || rng.gen::<f64>() < self.params.crossover_rate {
162                trial[j] = population[[r1, j]] + self.params.mutation_factor * (population[[r2, j]] - population[[r3, j]]);
163                trial[j] = trial[j].clamp(lower_bounds[j], upper_bounds[j]);
164            }
165        }
166        
167        trial
168    }
169    
170    /// DE/best/1/bin strategy
171    fn best_1_bin(&self, target_idx: usize, population: &Array2<f64>, best_idx: usize, rng: &mut impl Rng) -> Array1<f64> {
172        let dims = self.objective.dimensions();
173        let (lower_bounds, upper_bounds) = self.objective.bounds();
174        
175        // Select random indices different from target and best
176        let mut indices: Vec<usize> = (0..self.params.opt_params.population_size)
177            .filter(|&x| x != target_idx && x != best_idx)
178            .collect();
179        indices.shuffle(rng);
180        let (r1, r2) = (indices[0], indices[1]);
181        
182        // Create trial vector
183        let mut trial = population.row(target_idx).to_owned();
184        let j_rand = rng.gen_range(0..dims);
185        
186        for j in 0..dims {
187            if j == j_rand || rng.gen::<f64>() < self.params.crossover_rate {
188                trial[j] = population[[best_idx, j]] + self.params.mutation_factor * (population[[r1, j]] - population[[r2, j]]);
189                trial[j] = trial[j].clamp(lower_bounds[j], upper_bounds[j]);
190            }
191        }
192        
193        trial
194    }
195    
196    /// DE/rand/2/bin strategy
197    fn rand_2_bin(&self, target_idx: usize, population: &Array2<f64>, rng: &mut impl Rng) -> Array1<f64> {
198        let dims = self.objective.dimensions();
199        let (lower_bounds, upper_bounds) = self.objective.bounds();
200        
201        // Select random indices different from target
202        let mut indices: Vec<usize> = (0..self.params.opt_params.population_size)
203            .filter(|&x| x != target_idx)
204            .collect();
205        indices.shuffle(rng);
206        let (r1, r2, r3, r4, r5) = (indices[0], indices[1], indices[2], indices[3], indices[4]);
207        
208        // Create trial vector
209        let mut trial = population.row(target_idx).to_owned();
210        let j_rand = rng.gen_range(0..dims);
211        
212        for j in 0..dims {
213            if j == j_rand || rng.gen::<f64>() < self.params.crossover_rate {
214                trial[j] = population[[r1, j]] + 
215                    self.params.mutation_factor * (population[[r2, j]] - population[[r3, j]]) +
216                    self.params.mutation_factor * (population[[r4, j]] - population[[r5, j]]);
217                trial[j] = trial[j].clamp(lower_bounds[j], upper_bounds[j]);
218            }
219        }
220        
221        trial
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::test_function::{Sphere, TestFunction};
229    
230    #[test]
231    fn test_de_optimization() {
232        let problem = Sphere::new(2);
233        let params = DEParams::default();
234        let optimizer = DE::new(&problem, params);
235        
236        let result = optimizer.optimize().unwrap();
237        
238        // The global minimum of the sphere function is at (0,0) with f(x)=0
239        assert!(result.best_fitness < 1e-2); // Allow for some numerical error
240        
241        // Check if the solution is close to the known global minimum
242        let global_min = problem.global_minimum_position();
243        for (x, x_min) in result.best_solution.iter().zip(global_min.iter()) {
244            assert!((x - x_min).abs() < 1e-1);
245        }
246    }
247    
248    #[test]
249    fn test_de_strategies() {
250        let problem = Sphere::new(2);
251        
252        // Test all strategies
253        let strategies = [
254            DEStrategy::RandOneBin,
255            DEStrategy::BestOneBin,
256            DEStrategy::RandTwoBin,
257        ];
258        
259        for strategy in strategies.iter() {
260            let params = DEParams {
261                strategy: *strategy,
262                ..DEParams::default()
263            };
264            let optimizer = DE::new(&problem, params);
265            let result = optimizer.optimize().unwrap();
266            
267            // All strategies should be able to find the minimum
268            assert!(result.best_fitness < 1e-2);
269        }
270    }
271}