metaheurustics/algorithm/
sa.rs

1use ndarray::Array1;
2use rand::Rng;
3use crate::{MetaheuristicError, Result};
4use super::{ObjectiveFunction, OptimizationParams, OptimizationResult};
5
6/// Parameters specific to the Simulated Annealing algorithm
7#[derive(Debug, Clone)]
8pub struct SAParams {
9    /// General optimization parameters
10    pub opt_params: OptimizationParams,
11    /// Initial temperature
12    pub initial_temp: f64,
13    /// Cooling rate (alpha)
14    pub cooling_rate: f64,
15    /// Number of iterations at each temperature
16    pub iterations_per_temp: usize,
17    /// Minimum temperature
18    pub min_temp: f64,
19}
20
21impl Default for SAParams {
22    fn default() -> Self {
23        Self {
24            opt_params: OptimizationParams::default(),
25            initial_temp: 100.0,
26            cooling_rate: 0.95,
27            iterations_per_temp: 50,
28            min_temp: 1e-10,
29        }
30    }
31}
32
33/// Implementation of the Simulated Annealing algorithm
34pub struct SA<'a> {
35    objective: &'a dyn ObjectiveFunction,
36    params: SAParams,
37}
38
39impl<'a> SA<'a> {
40    /// Create a new instance of SA
41    pub fn new(objective: &'a dyn ObjectiveFunction, params: SAParams) -> Self {
42        Self { objective, params }
43    }
44    
45    /// Run the optimization algorithm
46    pub fn optimize(&self) -> Result<OptimizationResult> {
47        let mut rng = rand::thread_rng();
48        let (lower_bounds, upper_bounds) = self.objective.bounds();
49        let dims = self.objective.dimensions();
50        
51        // Initialize current solution
52        let mut current_solution = self.initialize_solution(&mut rng)?;
53        let mut current_energy = self.objective.evaluate(&current_solution);
54        
55        // Initialize best solution
56        let mut best_solution = current_solution.clone();
57        let mut best_energy = current_energy;
58        
59        let mut temp = self.params.initial_temp;
60        let mut iteration = 0;
61        let mut evaluations = 1;
62        
63        while temp > self.params.min_temp && iteration < self.params.opt_params.max_iterations {
64            for _ in 0..self.params.iterations_per_temp {
65                // Generate neighbor solution
66                let mut neighbor = current_solution.clone();
67                self.perturb_solution(&mut neighbor, temp, &mut rng);
68                
69                // Ensure bounds
70                for i in 0..dims {
71                    neighbor[i] = neighbor[i].clamp(lower_bounds[i], upper_bounds[i]);
72                }
73                
74                // Evaluate neighbor
75                let neighbor_energy = self.objective.evaluate(&neighbor);
76                evaluations += 1;
77                
78                // Calculate energy difference
79                let delta_e = neighbor_energy - current_energy;
80                
81                // Accept or reject new solution
82                if self.accept_solution(delta_e, temp, &mut rng) {
83                    current_solution = neighbor;
84                    current_energy = neighbor_energy;
85                    
86                    // Update best solution
87                    if current_energy < best_energy {
88                        best_solution = current_solution.clone();
89                        best_energy = current_energy;
90                    }
91                }
92            }
93            
94            // Cool down
95            temp *= self.params.cooling_rate;
96            iteration += 1;
97            
98            // Check termination criteria
99            if let Some(target) = self.params.opt_params.target_value {
100                if best_energy <= target {
101                    break;
102                }
103            }
104        }
105        
106        Ok(OptimizationResult {
107            best_solution,
108            best_fitness: best_energy,
109            iterations: iteration,
110            evaluations,
111        })
112    }
113    
114    /// Initialize a random solution
115    fn initialize_solution(&self, rng: &mut impl Rng) -> Result<Array1<f64>> {
116        let (lower_bounds, upper_bounds) = self.objective.bounds();
117        let dims = self.objective.dimensions();
118        
119        if lower_bounds.len() != dims || upper_bounds.len() != dims {
120            return Err(MetaheuristicError::InvalidDimension {
121                expected: dims,
122                got: lower_bounds.len(),
123            });
124        }
125        
126        let mut solution = Array1::zeros(dims);
127        for i in 0..dims {
128            solution[i] = rng.gen_range(lower_bounds[i]..=upper_bounds[i]);
129        }
130        
131        Ok(solution)
132    }
133    
134    /// Perturb the current solution
135    fn perturb_solution(&self, solution: &mut Array1<f64>, temp: f64, rng: &mut impl Rng) {
136        let (lower_bounds, upper_bounds) = self.objective.bounds();
137        
138        for i in 0..solution.len() {
139            // Scale perturbation with temperature
140            let range = (upper_bounds[i] - lower_bounds[i]) * temp / self.params.initial_temp;
141            let delta = (rng.gen::<f64>() - 0.5) * 2.0 * range;
142            solution[i] += delta;
143        }
144    }
145    
146    /// Decide whether to accept a solution
147    fn accept_solution(&self, delta_e: f64, temp: f64, rng: &mut impl Rng) -> bool {
148        if delta_e <= 0.0 {
149            true
150        } else {
151            let probability = (-delta_e / temp).exp();
152            rng.gen::<f64>() < probability
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::test_function::{Sphere, TestFunction};
161    
162    #[test]
163    fn test_sa_optimization() {
164        let problem = Sphere::new(2);
165        let params = SAParams::default();
166        let optimizer = SA::new(&problem, params);
167        
168        let result = optimizer.optimize().unwrap();
169        
170        // The global minimum of the sphere function is at (0,0) with f(x)=0
171        assert!(result.best_fitness < 1e-2); // Allow for some numerical error
172        
173        // Check if the solution is close to the known global minimum
174        let global_min = problem.global_minimum_position();
175        for (x, x_min) in result.best_solution.iter().zip(global_min.iter()) {
176            assert!((x - x_min).abs() < 1e-1);
177        }
178    }
179    
180    #[test]
181    fn test_sa_bounds() {
182        let problem = Sphere::new(2);
183        let params = SAParams {
184            opt_params: OptimizationParams {
185                max_iterations: 100,
186                ..Default::default()
187            },
188            ..Default::default()
189        };
190        let optimizer = SA::new(&problem, params);
191        
192        let result = optimizer.optimize().unwrap();
193        
194        // Check if solution is within bounds
195        let (lower_bounds, upper_bounds) = problem.bounds();
196        for (i, &x) in result.best_solution.iter().enumerate() {
197            assert!(x >= lower_bounds[i] && x <= upper_bounds[i]);
198        }
199    }
200    
201    #[test]
202    fn test_sa_temperature_decay() {
203        let problem = Sphere::new(2);
204        let params = SAParams {
205            initial_temp: 100.0,
206            cooling_rate: 0.8,
207            min_temp: 1.0,
208            ..Default::default()
209        };
210        let optimizer = SA::new(&problem, params);
211        
212        let result = optimizer.optimize().unwrap();
213        
214        // Verify that optimization completed
215        assert!(result.iterations > 0);
216        assert!(result.best_fitness < 1.0);
217    }
218}