scirs2_optimize/global/
simulated_annealing.rs1use crate::error::OptimizeError;
8use crate::unconstrained::OptimizeResult;
9use scirs2_core::ndarray::{Array1, ArrayView1};
10use scirs2_core::random::prelude::SliceRandom;
11use scirs2_core::random::rngs::StdRng;
12use scirs2_core::random::{rng, Rng, RngExt, SeedableRng};
13
14#[derive(Debug, Clone)]
16pub struct SimulatedAnnealingOptions {
17 pub maxiter: usize,
19 pub initial_temp: f64,
21 pub final_temp: f64,
23 pub alpha: f64,
25 pub max_steps_per_temp: usize,
27 pub step_size: f64,
29 pub seed: Option<u64>,
31 pub schedule: String,
33 pub verbose: bool,
35}
36
37impl Default for SimulatedAnnealingOptions {
38 fn default() -> Self {
39 Self {
40 maxiter: 10000,
41 initial_temp: 100.0,
42 final_temp: 1e-8,
43 alpha: 0.95,
44 max_steps_per_temp: 100,
45 step_size: 0.5,
46 seed: None,
47 schedule: "exponential".to_string(),
48 verbose: false,
49 }
50 }
51}
52
53pub type Bounds = Vec<(f64, f64)>;
55
56pub struct SimulatedAnnealing<F>
58where
59 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
60{
61 func: F,
62 x0: Array1<f64>,
63 bounds: Option<Bounds>,
64 options: SimulatedAnnealingOptions,
65 ndim: usize,
66 current_x: Array1<f64>,
67 current_value: f64,
68 best_x: Array1<f64>,
69 best_value: f64,
70 temperature: f64,
71 rng: StdRng,
72 nfev: usize,
73 nit: usize,
74 recent_accepted: usize,
76 recent_total: usize,
78}
79
80impl<F> SimulatedAnnealing<F>
81where
82 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
83{
84 pub fn new(
86 func: F,
87 x0: Array1<f64>,
88 bounds: Option<Bounds>,
89 options: SimulatedAnnealingOptions,
90 ) -> Self {
91 let ndim = x0.len();
92 let seed = options.seed.unwrap_or_else(|| rng().random());
93 let rng = StdRng::seed_from_u64(seed);
94
95 let initial_value = func(&x0.view());
97
98 Self {
99 func,
100 x0: x0.clone(),
101 bounds,
102 options: options.clone(),
103 ndim,
104 current_x: x0.clone(),
105 current_value: initial_value,
106 best_x: x0,
107 best_value: initial_value,
108 temperature: options.initial_temp,
109 rng,
110 nfev: 1,
111 nit: 0,
112 recent_accepted: 0,
113 recent_total: 0,
114 }
115 }
116
117 fn generate_neighbor(&mut self) -> Array1<f64> {
119 let mut neighbor = self.current_x.clone();
120
121 let num_dims_to_perturb = self.rng.random_range(1..=self.ndim);
123 let mut dims: Vec<usize> = (0..self.ndim).collect();
124 dims.shuffle(&mut self.rng);
125
126 for &i in dims.iter().take(num_dims_to_perturb) {
127 let perturbation = self
128 .rng
129 .random_range(-self.options.step_size..self.options.step_size);
130 neighbor[i] += perturbation;
131
132 if let Some(ref bounds) = self.bounds {
134 let (lb, ub) = bounds[i];
135 neighbor[i] = neighbor[i].max(lb).min(ub);
136 }
137 }
138
139 neighbor
140 }
141
142 fn acceptance_probability(&self, new_value: f64) -> f64 {
144 if new_value < self.current_value {
145 1.0
146 } else {
147 let delta = new_value - self.current_value;
148 (-delta / self.temperature).exp()
149 }
150 }
151
152 fn update_temperature(&mut self) {
154 match self.options.schedule.as_str() {
155 "exponential" => {
156 self.temperature *= self.options.alpha;
157 }
158 "linear" => {
159 let temp_range = self.options.initial_temp - self.options.final_temp;
160 let temp_decrement = temp_range / self.options.maxiter as f64;
161 self.temperature = (self.temperature - temp_decrement).max(self.options.final_temp);
162 }
163 "adaptive" => {
164 let acceptance_rate = self.calculate_acceptance_rate();
166 if acceptance_rate > 0.8 {
167 self.temperature *= 0.9; } else if acceptance_rate < 0.2 {
169 self.temperature *= 0.99; } else {
171 self.temperature *= self.options.alpha;
172 }
173 }
174 _ => {
175 self.temperature *= self.options.alpha; }
177 }
178 }
179
180 fn calculate_acceptance_rate(&self) -> f64 {
182 if self.recent_total == 0 {
183 0.0
187 } else {
188 self.recent_accepted as f64 / self.recent_total as f64
189 }
190 }
191
192 fn step(&mut self) -> bool {
194 self.nit += 1;
195
196 let mut accepted = 0usize;
197 for _ in 0..self.options.max_steps_per_temp {
198 let neighbor = self.generate_neighbor();
200 let neighbor_value = (self.func)(&neighbor.view());
201 self.nfev += 1;
202
203 let acceptance_prob = self.acceptance_probability(neighbor_value);
205 if self.rng.random_range(0.0..1.0) < acceptance_prob {
206 accepted += 1;
207 self.current_x = neighbor;
208 self.current_value = neighbor_value;
209
210 if neighbor_value < self.best_value {
212 self.best_x = self.current_x.clone();
213 self.best_value = neighbor_value;
214 }
215 }
216 }
217
218 self.recent_accepted = accepted;
221 self.recent_total = self.options.max_steps_per_temp;
222
223 self.update_temperature();
225
226 self.temperature < self.options.final_temp
228 }
229
230 pub fn run(&mut self) -> OptimizeResult<f64> {
232 let mut converged = false;
233
234 while self.nit < self.options.maxiter {
235 converged = self.step();
236
237 if converged {
238 break;
239 }
240
241 if self.options.verbose && self.nit.is_multiple_of(100) {
242 println!(
243 "Iteration {}: T = {:.6}..best = {:.6}",
244 self.nit, self.temperature, self.best_value
245 );
246 }
247 }
248
249 OptimizeResult {
250 x: self.best_x.clone(),
251 fun: self.best_value,
252 nfev: self.nfev,
253 func_evals: self.nfev,
254 nit: self.nit,
255 success: converged,
256 message: if converged {
257 "Temperature reached minimum"
258 } else {
259 "Maximum iterations reached"
260 }
261 .to_string(),
262 ..Default::default()
263 }
264 }
265}
266
267#[allow(dead_code)]
269pub fn simulated_annealing<F>(
270 func: F,
271 x0: Array1<f64>,
272 bounds: Option<Bounds>,
273 options: Option<SimulatedAnnealingOptions>,
274) -> Result<OptimizeResult<f64>, OptimizeError>
275where
276 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
277{
278 let options = options.unwrap_or_default();
279 let mut solver = SimulatedAnnealing::new(func, x0, bounds, options);
280 Ok(solver.run())
281}