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