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, 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}
75
76impl<F> SimulatedAnnealing<F>
77where
78 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
79{
80 pub fn new(
82 func: F,
83 x0: Array1<f64>,
84 bounds: Option<Bounds>,
85 options: SimulatedAnnealingOptions,
86 ) -> Self {
87 let ndim = x0.len();
88 let seed = options.seed.unwrap_or_else(|| rng().random());
89 let rng = StdRng::seed_from_u64(seed);
90
91 let initial_value = func(&x0.view());
93
94 Self {
95 func,
96 x0: x0.clone(),
97 bounds,
98 options: options.clone(),
99 ndim,
100 current_x: x0.clone(),
101 current_value: initial_value,
102 best_x: x0,
103 best_value: initial_value,
104 temperature: options.initial_temp,
105 rng,
106 nfev: 1,
107 nit: 0,
108 }
109 }
110
111 fn generate_neighbor(&mut self) -> Array1<f64> {
113 let mut neighbor = self.current_x.clone();
114
115 let num_dims_to_perturb = self.rng.gen_range(1..=self.ndim);
117 let mut dims: Vec<usize> = (0..self.ndim).collect();
118 dims.shuffle(&mut self.rng);
119
120 for &i in dims.iter().take(num_dims_to_perturb) {
121 let perturbation = self
122 .rng
123 .random_range(-self.options.step_size..self.options.step_size);
124 neighbor[i] += perturbation;
125
126 if let Some(ref bounds) = self.bounds {
128 let (lb, ub) = bounds[i];
129 neighbor[i] = neighbor[i].max(lb).min(ub);
130 }
131 }
132
133 neighbor
134 }
135
136 fn acceptance_probability(&self, new_value: f64) -> f64 {
138 if new_value < self.current_value {
139 1.0
140 } else {
141 let delta = new_value - self.current_value;
142 (-delta / self.temperature).exp()
143 }
144 }
145
146 fn update_temperature(&mut self) {
148 match self.options.schedule.as_str() {
149 "exponential" => {
150 self.temperature *= self.options.alpha;
151 }
152 "linear" => {
153 let temp_range = self.options.initial_temp - self.options.final_temp;
154 let temp_decrement = temp_range / self.options.maxiter as f64;
155 self.temperature = (self.temperature - temp_decrement).max(self.options.final_temp);
156 }
157 "adaptive" => {
158 let acceptance_rate = self.calculate_acceptance_rate();
160 if acceptance_rate > 0.8 {
161 self.temperature *= 0.9; } else if acceptance_rate < 0.2 {
163 self.temperature *= 0.99; } else {
165 self.temperature *= self.options.alpha;
166 }
167 }
168 _ => {
169 self.temperature *= self.options.alpha; }
171 }
172 }
173
174 fn calculate_acceptance_rate(&self) -> f64 {
176 0.5
179 }
180
181 fn step(&mut self) -> bool {
183 self.nit += 1;
184
185 for _ in 0..self.options.max_steps_per_temp {
186 let neighbor = self.generate_neighbor();
188 let neighbor_value = (self.func)(&neighbor.view());
189 self.nfev += 1;
190
191 let acceptance_prob = self.acceptance_probability(neighbor_value);
193 if self.rng.gen_range(0.0..1.0) < acceptance_prob {
194 self.current_x = neighbor;
195 self.current_value = neighbor_value;
196
197 if neighbor_value < self.best_value {
199 self.best_x = self.current_x.clone();
200 self.best_value = neighbor_value;
201 }
202 }
203 }
204
205 self.update_temperature();
207
208 self.temperature < self.options.final_temp
210 }
211
212 pub fn run(&mut self) -> OptimizeResult<f64> {
214 let mut converged = false;
215
216 while self.nit < self.options.maxiter {
217 converged = self.step();
218
219 if converged {
220 break;
221 }
222
223 if self.options.verbose && self.nit.is_multiple_of(100) {
224 println!(
225 "Iteration {}: T = {:.6}..best = {:.6}",
226 self.nit, self.temperature, self.best_value
227 );
228 }
229 }
230
231 OptimizeResult {
232 x: self.best_x.clone(),
233 fun: self.best_value,
234 nfev: self.nfev,
235 func_evals: self.nfev,
236 nit: 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
249#[allow(dead_code)]
251pub fn simulated_annealing<F>(
252 func: F,
253 x0: Array1<f64>,
254 bounds: Option<Bounds>,
255 options: Option<SimulatedAnnealingOptions>,
256) -> Result<OptimizeResult<f64>, OptimizeError>
257where
258 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
259{
260 let options = options.unwrap_or_default();
261 let mut solver = SimulatedAnnealing::new(func, x0, bounds, options);
262 Ok(solver.run())
263}