scirs2_optimize/global/
simulated_annealing.rs

1//! Simulated Annealing algorithm for global optimization
2//!
3//! Simulated annealing is a probabilistic optimization algorithm inspired by
4//! the physical process of annealing in metallurgy. It accepts worse solutions
5//! with a probability that decreases over time as the "temperature" cools.
6
7use 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/// Options for Simulated Annealing
15#[derive(Debug, Clone)]
16pub struct SimulatedAnnealingOptions {
17    /// Maximum number of iterations
18    pub maxiter: usize,
19    /// Initial temperature
20    pub initial_temp: f64,
21    /// Final temperature
22    pub final_temp: f64,
23    /// Temperature reduction rate (0 < alpha < 1)
24    pub alpha: f64,
25    /// Maximum steps at each temperature
26    pub max_steps_per_temp: usize,
27    /// Step size for neighbor generation
28    pub step_size: f64,
29    /// Random seed for reproducibility
30    pub seed: Option<u64>,
31    /// Cooling schedule: "exponential", "linear", or "adaptive"
32    pub schedule: String,
33    /// Whether to print progress
34    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
53/// Bounds for variables
54pub type Bounds = Vec<(f64, f64)>;
55
56/// Simulated Annealing solver
57pub 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    /// Create new Simulated Annealing solver
81    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        // Evaluate initial point
92        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    /// Generate a random neighbor
112    fn generate_neighbor(&mut self) -> Array1<f64> {
113        let mut neighbor = self.current_x.clone();
114
115        // Randomly perturb one or more dimensions
116        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            // Apply bounds if specified
127            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    /// Calculate acceptance probability
137    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    /// Update temperature according to cooling schedule
147    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                // Adaptive cooling based on acceptance rate
159                let acceptance_rate = self.calculate_acceptance_rate();
160                if acceptance_rate > 0.8 {
161                    self.temperature *= 0.9; // Cool faster if accepting too many
162                } else if acceptance_rate < 0.2 {
163                    self.temperature *= 0.99; // Cool slower if accepting too few
164                } else {
165                    self.temperature *= self.options.alpha;
166                }
167            }
168            _ => {
169                self.temperature *= self.options.alpha; // Default to exponential
170            }
171        }
172    }
173
174    /// Calculate recent acceptance rate (simplified)
175    fn calculate_acceptance_rate(&self) -> f64 {
176        // In a real implementation, this would track actual acceptance rate
177        // For now, return a default value
178        0.5
179    }
180
181    /// Run one step of the algorithm
182    fn step(&mut self) -> bool {
183        self.nit += 1;
184
185        for _ in 0..self.options.max_steps_per_temp {
186            // Generate neighbor
187            let neighbor = self.generate_neighbor();
188            let neighbor_value = (self.func)(&neighbor.view());
189            self.nfev += 1;
190
191            // Accept or reject
192            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                // Update best if improved
198                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        // Update temperature
206        self.update_temperature();
207
208        // Check convergence
209        self.temperature < self.options.final_temp
210    }
211
212    /// Run the simulated annealing algorithm
213    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/// Perform global optimization using simulated annealing
250#[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}