scirs2_optimize/global/
simulated_annealing.rs1use crate::error::OptimizeError;
8use crate::unconstrained::OptimizeResult;
9use ndarray::{Array1, ArrayView1};
10use rand::prelude::*;
11use rand::rngs::StdRng;
12
13#[derive(Debug, Clone)]
15pub struct SimulatedAnnealingOptions {
16 pub maxiter: usize,
18 pub initial_temp: f64,
20 pub final_temp: f64,
22 pub alpha: f64,
24 pub max_steps_per_temp: usize,
26 pub step_size: f64,
28 pub seed: Option<u64>,
30 pub schedule: String,
32 pub verbose: bool,
34}
35
36impl Default for SimulatedAnnealingOptions {
37 fn default() -> Self {
38 Self {
39 maxiter: 10000,
40 initial_temp: 100.0,
41 final_temp: 1e-8,
42 alpha: 0.95,
43 max_steps_per_temp: 100,
44 step_size: 0.5,
45 seed: None,
46 schedule: "exponential".to_string(),
47 verbose: false,
48 }
49 }
50}
51
52pub type Bounds = Vec<(f64, f64)>;
54
55pub struct SimulatedAnnealing<F>
57where
58 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
59{
60 func: F,
61 x0: Array1<f64>,
62 bounds: Option<Bounds>,
63 options: SimulatedAnnealingOptions,
64 ndim: usize,
65 current_x: Array1<f64>,
66 current_value: f64,
67 best_x: Array1<f64>,
68 best_value: f64,
69 temperature: f64,
70 rng: StdRng,
71 nfev: usize,
72 nit: usize,
73}
74
75impl<F> SimulatedAnnealing<F>
76where
77 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
78{
79 pub fn new(
81 func: F,
82 x0: Array1<f64>,
83 bounds: Option<Bounds>,
84 options: SimulatedAnnealingOptions,
85 ) -> Self {
86 let ndim = x0.len();
87 let seed = options.seed.unwrap_or_else(rand::random);
88 let rng = StdRng::seed_from_u64(seed);
89
90 let initial_value = func(&x0.view());
92
93 Self {
94 func,
95 x0: x0.clone(),
96 bounds,
97 options: options.clone(),
98 ndim,
99 current_x: x0.clone(),
100 current_value: initial_value,
101 best_x: x0,
102 best_value: initial_value,
103 temperature: options.initial_temp,
104 rng,
105 nfev: 1,
106 nit: 0,
107 }
108 }
109
110 fn generate_neighbor(&mut self) -> Array1<f64> {
112 let mut neighbor = self.current_x.clone();
113
114 let num_dims_to_perturb = self.rng.random_range(1..=self.ndim);
116 let mut dims: Vec<usize> = (0..self.ndim).collect();
117 dims.shuffle(&mut self.rng);
118
119 for &i in dims.iter().take(num_dims_to_perturb) {
120 let perturbation = self
121 .rng
122 .random_range(-self.options.step_size..self.options.step_size);
123 neighbor[i] += perturbation;
124
125 if let Some(ref bounds) = self.bounds {
127 let (lb, ub) = bounds[i];
128 neighbor[i] = neighbor[i].max(lb).min(ub);
129 }
130 }
131
132 neighbor
133 }
134
135 fn acceptance_probability(&self, new_value: f64) -> f64 {
137 if new_value < self.current_value {
138 1.0
139 } else {
140 let delta = new_value - self.current_value;
141 (-delta / self.temperature).exp()
142 }
143 }
144
145 fn update_temperature(&mut self) {
147 match self.options.schedule.as_str() {
148 "exponential" => {
149 self.temperature *= self.options.alpha;
150 }
151 "linear" => {
152 let temp_range = self.options.initial_temp - self.options.final_temp;
153 let temp_decrement = temp_range / self.options.maxiter as f64;
154 self.temperature = (self.temperature - temp_decrement).max(self.options.final_temp);
155 }
156 "adaptive" => {
157 let acceptance_rate = self.calculate_acceptance_rate();
159 if acceptance_rate > 0.8 {
160 self.temperature *= 0.9; } else if acceptance_rate < 0.2 {
162 self.temperature *= 0.99; } else {
164 self.temperature *= self.options.alpha;
165 }
166 }
167 _ => {
168 self.temperature *= self.options.alpha; }
170 }
171 }
172
173 fn calculate_acceptance_rate(&self) -> f64 {
175 0.5
178 }
179
180 fn step(&mut self) -> bool {
182 self.nit += 1;
183
184 for _ in 0..self.options.max_steps_per_temp {
185 let neighbor = self.generate_neighbor();
187 let neighbor_value = (self.func)(&neighbor.view());
188 self.nfev += 1;
189
190 let acceptance_prob = self.acceptance_probability(neighbor_value);
192 if self.rng.random::<f64>() < acceptance_prob {
193 self.current_x = neighbor;
194 self.current_value = neighbor_value;
195
196 if neighbor_value < self.best_value {
198 self.best_x = self.current_x.clone();
199 self.best_value = neighbor_value;
200 }
201 }
202 }
203
204 self.update_temperature();
206
207 self.temperature < self.options.final_temp
209 }
210
211 pub fn run(&mut self) -> OptimizeResult<f64> {
213 let mut converged = false;
214
215 while self.nit < self.options.maxiter {
216 converged = self.step();
217
218 if converged {
219 break;
220 }
221
222 if self.options.verbose && self.nit % 100 == 0 {
223 println!(
224 "Iteration {}: T = {:.6}, best = {:.6}",
225 self.nit, self.temperature, self.best_value
226 );
227 }
228 }
229
230 OptimizeResult {
231 x: self.best_x.clone(),
232 fun: self.best_value,
233 nfev: self.nfev,
234 func_evals: self.nfev,
235 nit: self.nit,
236 iterations: self.nit,
237 success: converged,
238 message: if converged {
239 "Temperature reached minimum"
240 } else {
241 "Maximum iterations reached"
242 }
243 .to_string(),
244 ..Default::default()
245 }
246 }
247}
248
249pub fn simulated_annealing<F>(
251 func: F,
252 x0: Array1<f64>,
253 bounds: Option<Bounds>,
254 options: Option<SimulatedAnnealingOptions>,
255) -> Result<OptimizeResult<f64>, OptimizeError>
256where
257 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
258{
259 let options = options.unwrap_or_default();
260 let mut solver = SimulatedAnnealing::new(func, x0, bounds, options);
261 Ok(solver.run())
262}