metaheurustics/algorithm/
sa.rs1use ndarray::Array1;
2use rand::Rng;
3use crate::{MetaheuristicError, Result};
4use super::{ObjectiveFunction, OptimizationParams, OptimizationResult};
5
6#[derive(Debug, Clone)]
8pub struct SAParams {
9 pub opt_params: OptimizationParams,
11 pub initial_temp: f64,
13 pub cooling_rate: f64,
15 pub iterations_per_temp: usize,
17 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
33pub struct SA<'a> {
35 objective: &'a dyn ObjectiveFunction,
36 params: SAParams,
37}
38
39impl<'a> SA<'a> {
40 pub fn new(objective: &'a dyn ObjectiveFunction, params: SAParams) -> Self {
42 Self { objective, params }
43 }
44
45 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 let mut current_solution = self.initialize_solution(&mut rng)?;
53 let mut current_energy = self.objective.evaluate(¤t_solution);
54
55 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 let mut neighbor = current_solution.clone();
67 self.perturb_solution(&mut neighbor, temp, &mut rng);
68
69 for i in 0..dims {
71 neighbor[i] = neighbor[i].clamp(lower_bounds[i], upper_bounds[i]);
72 }
73
74 let neighbor_energy = self.objective.evaluate(&neighbor);
76 evaluations += 1;
77
78 let delta_e = neighbor_energy - current_energy;
80
81 if self.accept_solution(delta_e, temp, &mut rng) {
83 current_solution = neighbor;
84 current_energy = neighbor_energy;
85
86 if current_energy < best_energy {
88 best_solution = current_solution.clone();
89 best_energy = current_energy;
90 }
91 }
92 }
93
94 temp *= self.params.cooling_rate;
96 iteration += 1;
97
98 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 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 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 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 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 assert!(result.best_fitness < 1e-2); 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 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 assert!(result.iterations > 0);
216 assert!(result.best_fitness < 1.0);
217 }
218}