use crate::error::OptimizeError;
use crate::unconstrained::OptimizeResult;
use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::random::prelude::SliceRandom;
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{rng, Rng, RngExt, SeedableRng};
#[derive(Debug, Clone)]
pub struct SimulatedAnnealingOptions {
pub maxiter: usize,
pub initial_temp: f64,
pub final_temp: f64,
pub alpha: f64,
pub max_steps_per_temp: usize,
pub step_size: f64,
pub seed: Option<u64>,
pub schedule: String,
pub verbose: bool,
}
impl Default for SimulatedAnnealingOptions {
fn default() -> Self {
Self {
maxiter: 10000,
initial_temp: 100.0,
final_temp: 1e-8,
alpha: 0.95,
max_steps_per_temp: 100,
step_size: 0.5,
seed: None,
schedule: "exponential".to_string(),
verbose: false,
}
}
}
pub type Bounds = Vec<(f64, f64)>;
pub struct SimulatedAnnealing<F>
where
F: Fn(&ArrayView1<f64>) -> f64 + Clone,
{
func: F,
x0: Array1<f64>,
bounds: Option<Bounds>,
options: SimulatedAnnealingOptions,
ndim: usize,
current_x: Array1<f64>,
current_value: f64,
best_x: Array1<f64>,
best_value: f64,
temperature: f64,
rng: StdRng,
nfev: usize,
nit: usize,
}
impl<F> SimulatedAnnealing<F>
where
F: Fn(&ArrayView1<f64>) -> f64 + Clone,
{
pub fn new(
func: F,
x0: Array1<f64>,
bounds: Option<Bounds>,
options: SimulatedAnnealingOptions,
) -> Self {
let ndim = x0.len();
let seed = options.seed.unwrap_or_else(|| rng().random());
let rng = StdRng::seed_from_u64(seed);
let initial_value = func(&x0.view());
Self {
func,
x0: x0.clone(),
bounds,
options: options.clone(),
ndim,
current_x: x0.clone(),
current_value: initial_value,
best_x: x0,
best_value: initial_value,
temperature: options.initial_temp,
rng,
nfev: 1,
nit: 0,
}
}
fn generate_neighbor(&mut self) -> Array1<f64> {
let mut neighbor = self.current_x.clone();
let num_dims_to_perturb = self.rng.random_range(1..=self.ndim);
let mut dims: Vec<usize> = (0..self.ndim).collect();
dims.shuffle(&mut self.rng);
for &i in dims.iter().take(num_dims_to_perturb) {
let perturbation = self
.rng
.random_range(-self.options.step_size..self.options.step_size);
neighbor[i] += perturbation;
if let Some(ref bounds) = self.bounds {
let (lb, ub) = bounds[i];
neighbor[i] = neighbor[i].max(lb).min(ub);
}
}
neighbor
}
fn acceptance_probability(&self, new_value: f64) -> f64 {
if new_value < self.current_value {
1.0
} else {
let delta = new_value - self.current_value;
(-delta / self.temperature).exp()
}
}
fn update_temperature(&mut self) {
match self.options.schedule.as_str() {
"exponential" => {
self.temperature *= self.options.alpha;
}
"linear" => {
let temp_range = self.options.initial_temp - self.options.final_temp;
let temp_decrement = temp_range / self.options.maxiter as f64;
self.temperature = (self.temperature - temp_decrement).max(self.options.final_temp);
}
"adaptive" => {
let acceptance_rate = self.calculate_acceptance_rate();
if acceptance_rate > 0.8 {
self.temperature *= 0.9; } else if acceptance_rate < 0.2 {
self.temperature *= 0.99; } else {
self.temperature *= self.options.alpha;
}
}
_ => {
self.temperature *= self.options.alpha; }
}
}
fn calculate_acceptance_rate(&self) -> f64 {
0.5
}
fn step(&mut self) -> bool {
self.nit += 1;
for _ in 0..self.options.max_steps_per_temp {
let neighbor = self.generate_neighbor();
let neighbor_value = (self.func)(&neighbor.view());
self.nfev += 1;
let acceptance_prob = self.acceptance_probability(neighbor_value);
if self.rng.random_range(0.0..1.0) < acceptance_prob {
self.current_x = neighbor;
self.current_value = neighbor_value;
if neighbor_value < self.best_value {
self.best_x = self.current_x.clone();
self.best_value = neighbor_value;
}
}
}
self.update_temperature();
self.temperature < self.options.final_temp
}
pub fn run(&mut self) -> OptimizeResult<f64> {
let mut converged = false;
while self.nit < self.options.maxiter {
converged = self.step();
if converged {
break;
}
if self.options.verbose && self.nit.is_multiple_of(100) {
println!(
"Iteration {}: T = {:.6}..best = {:.6}",
self.nit, self.temperature, self.best_value
);
}
}
OptimizeResult {
x: self.best_x.clone(),
fun: self.best_value,
nfev: self.nfev,
func_evals: self.nfev,
nit: self.nit,
success: converged,
message: if converged {
"Temperature reached minimum"
} else {
"Maximum iterations reached"
}
.to_string(),
..Default::default()
}
}
}
#[allow(dead_code)]
pub fn simulated_annealing<F>(
func: F,
x0: Array1<f64>,
bounds: Option<Bounds>,
options: Option<SimulatedAnnealingOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: Fn(&ArrayView1<f64>) -> f64 + Clone,
{
let options = options.unwrap_or_default();
let mut solver = SimulatedAnnealing::new(func, x0, bounds, options);
Ok(solver.run())
}