use super::{compute_norm, OptimizeResult};
use crate::error::{NumRs2Error, Result};
use num_traits::Float;
use scirs2_core::random::*;
#[derive(Debug, Clone, Copy)]
pub enum CoolingSchedule {
Exponential,
Linear,
Logarithmic,
Adaptive,
}
#[derive(Debug, Clone, Copy)]
pub enum NeighborStrategy {
Gaussian,
Cauchy,
Uniform,
}
#[derive(Debug, Clone)]
pub struct SAConfig<T: Float> {
pub initial_temp: T,
pub cooling_rate: T,
pub linear_delta: T,
pub max_iter: usize,
pub neighbor_sigma: T,
pub cooling_schedule: CoolingSchedule,
pub neighbor_strategy: NeighborStrategy,
pub ftol: T,
pub stagnation_limit: usize,
}
impl<T: Float> Default for SAConfig<T> {
fn default() -> Self {
Self {
initial_temp: T::from(100.0).expect("100.0 should be representable"),
cooling_rate: T::from(0.95).expect("0.95 should be representable"),
linear_delta: T::from(0.1).expect("0.1 should be representable"),
max_iter: 10000,
neighbor_sigma: T::from(1.0).expect("1.0 should be representable"),
cooling_schedule: CoolingSchedule::Exponential,
neighbor_strategy: NeighborStrategy::Gaussian,
ftol: T::from(1e-9).expect("1e-9 should be representable"),
stagnation_limit: 500,
}
}
}
pub fn simulated_annealing<T, F>(
f: F,
x0: &[T],
config: Option<SAConfig<T>>,
) -> Result<OptimizeResult<T>>
where
T: Float + std::fmt::Debug + std::iter::Sum,
F: Fn(&[T]) -> T,
{
let cfg = config.unwrap_or_default();
let n = x0.len();
let mut x_current = x0.to_vec();
let mut f_current = f(&x_current);
let mut x_best = x_current.clone();
let mut f_best = f_current;
let mut nfev = 1;
let mut temperature = cfg.initial_temp;
let mut rng = thread_rng();
let mut accepted_count = 0;
let mut total_count = 0;
let mut stagnation_counter = 0;
let mut last_improvement = f_best;
for iteration in 0..cfg.max_iter {
let x_new = generate_neighbor(
&x_current,
cfg.neighbor_strategy,
cfg.neighbor_sigma,
&mut rng,
)?;
let f_new = f(&x_new);
nfev += 1;
let delta_f = f_new - f_current;
let accept = if delta_f < T::zero() {
true
} else {
let acceptance_prob = (-delta_f / temperature).exp();
let uniform = Uniform::new(0.0f64, 1.0f64).map_err(|e| {
NumRs2Error::ConversionError(format!(
"Failed to create uniform distribution: {}",
e
))
})?;
let rand_val = T::from(uniform.sample(&mut rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert random value".to_string())
})?;
rand_val < acceptance_prob
};
if accept {
x_current = x_new;
f_current = f_new;
accepted_count += 1;
if f_new < f_best {
x_best = x_current.clone();
f_best = f_new;
stagnation_counter = 0;
last_improvement = f_best;
}
}
total_count += 1;
if (f_best - last_improvement).abs() < cfg.ftol {
stagnation_counter += 1;
if stagnation_counter >= cfg.stagnation_limit {
let grad = finite_difference_gradient(&f, &x_best)?;
return Ok(OptimizeResult {
x: x_best,
fun: f_best,
grad,
nit: iteration + 1,
nfev,
njev: 0, success: true,
message: format!(
"Optimization terminated successfully (stagnation at iteration {})",
iteration
),
});
}
}
temperature = update_temperature(
cfg.initial_temp,
iteration + 1,
&cfg.cooling_schedule,
cfg.cooling_rate,
cfg.linear_delta,
accepted_count,
total_count,
)?;
if (iteration + 1) % 100 == 0 {
accepted_count = 0;
total_count = 0;
}
if temperature
< T::from(1e-10).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert temperature threshold".to_string())
})?
{
let grad = finite_difference_gradient(&f, &x_best)?;
return Ok(OptimizeResult {
x: x_best,
fun: f_best,
grad,
nit: iteration + 1,
nfev,
njev: 0,
success: true,
message: format!(
"Optimization terminated (temperature reached minimum at iteration {})",
iteration
),
});
}
}
let grad = finite_difference_gradient(&f, &x_best)?;
Ok(OptimizeResult {
x: x_best,
fun: f_best,
grad,
nit: cfg.max_iter,
nfev,
njev: 0,
success: false,
message: format!("Maximum iterations ({}) reached", cfg.max_iter),
})
}
fn generate_neighbor<T: Float>(
x: &[T],
strategy: NeighborStrategy,
sigma: T,
rng: &mut impl Rng,
) -> Result<Vec<T>> {
let n = x.len();
let mut x_new = vec![T::zero(); n];
match strategy {
NeighborStrategy::Gaussian => {
let sigma_f64 = sigma.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert sigma to f64".to_string())
})?;
let normal = Normal::new(0.0, sigma_f64).map_err(|e| {
NumRs2Error::ComputationError(format!(
"Failed to create normal distribution: {}",
e
))
})?;
for i in 0..n {
let perturbation = T::from(normal.sample(rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert perturbation".to_string())
})?;
x_new[i] = x[i] + perturbation;
}
}
NeighborStrategy::Cauchy => {
let gamma_f64 = sigma.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert gamma to f64".to_string())
})?;
let cauchy = Cauchy::new(0.0, gamma_f64).map_err(|e| {
NumRs2Error::ComputationError(format!(
"Failed to create Cauchy distribution: {}",
e
))
})?;
for i in 0..n {
let perturbation = T::from(cauchy.sample(rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert perturbation".to_string())
})?;
x_new[i] = x[i] + perturbation;
}
}
NeighborStrategy::Uniform => {
let delta_f64 = sigma.to_f64().ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert delta to f64".to_string())
})?;
let uniform = Uniform::new(-delta_f64, delta_f64).map_err(|e| {
NumRs2Error::ConversionError(format!(
"Failed to create uniform distribution: {}",
e
))
})?;
for i in 0..n {
let perturbation = T::from(uniform.sample(rng)).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert perturbation".to_string())
})?;
x_new[i] = x[i] + perturbation;
}
}
}
Ok(x_new)
}
fn update_temperature<T: Float>(
initial_temp: T,
iteration: usize,
schedule: &CoolingSchedule,
cooling_rate: T,
linear_delta: T,
accepted_count: usize,
total_count: usize,
) -> Result<T> {
let k = T::from(iteration).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert iteration to temperature type".to_string())
})?;
match schedule {
CoolingSchedule::Exponential => {
Ok(initial_temp * cooling_rate.powi(iteration as i32))
}
CoolingSchedule::Linear => {
let temp = initial_temp - k * linear_delta;
Ok(temp.max(T::from(1e-10).ok_or_else(|| {
NumRs2Error::ConversionError("Failed to convert min temperature".to_string())
})?))
}
CoolingSchedule::Logarithmic => {
let two = T::from(2.0)
.ok_or_else(|| NumRs2Error::ConversionError("Failed to convert 2.0".to_string()))?;
let log_term = (k + two).ln();
Ok(initial_temp / log_term)
}
CoolingSchedule::Adaptive => {
let acceptance_ratio = if total_count > 0 {
accepted_count as f64 / total_count as f64
} else {
0.5 };
let adaptive_rate = if acceptance_ratio < 0.2 {
cooling_rate
* T::from(0.98).ok_or_else(|| {
NumRs2Error::ConversionError(
"Failed to convert adaptive factor".to_string(),
)
})?
} else if acceptance_ratio > 0.8 {
cooling_rate
* T::from(1.02).ok_or_else(|| {
NumRs2Error::ConversionError(
"Failed to convert adaptive factor".to_string(),
)
})?
} else {
cooling_rate
};
Ok(initial_temp * adaptive_rate.powi(iteration as i32))
}
}
}
fn finite_difference_gradient<T, F>(f: &F, x: &[T]) -> Result<Vec<T>>
where
T: Float + std::fmt::Debug,
F: Fn(&[T]) -> T,
{
let n = x.len();
let h = T::from(1e-8)
.ok_or_else(|| NumRs2Error::ConversionError("Failed to convert step size".to_string()))?;
let mut grad = vec![T::zero(); n];
for i in 0..n {
let mut x_plus = x.to_vec();
let mut x_minus = x.to_vec();
x_plus[i] = x[i] + h;
x_minus[i] = x[i] - h;
let f_plus = f(&x_plus);
let f_minus = f(&x_minus);
let two = T::from(2.0)
.ok_or_else(|| NumRs2Error::ConversionError("Failed to convert 2.0".to_string()))?;
grad[i] = (f_plus - f_minus) / (two * h);
}
Ok(grad)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_simulated_annealing_quadratic() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let config = SAConfig {
max_iter: 10000, initial_temp: 100.0,
cooling_rate: 0.99, neighbor_sigma: 1.0, ftol: 1e-10,
stagnation_limit: 500, ..Default::default()
};
let result = simulated_annealing(f, &[5.0, 5.0], Some(config))
.expect("SA should succeed for quadratic");
assert!(
result.fun < 0.1,
"Should find near-optimal solution, got fun = {}",
result.fun
);
assert_relative_eq!(result.x[0], 0.0, epsilon = 0.5);
assert_relative_eq!(result.x[1], 0.0, epsilon = 0.5);
}
#[test]
fn test_simulated_annealing_rastrigin() {
let rastrigin = |x: &[f64]| {
let n = x.len() as f64;
10.0 * n
+ x.iter()
.map(|&xi| xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos())
.sum::<f64>()
};
let config = SAConfig {
max_iter: 15000, initial_temp: 150.0, cooling_rate: 0.97, neighbor_sigma: 1.0, stagnation_limit: 500, ..Default::default()
};
let result = simulated_annealing(rastrigin, &[2.5, 2.5], Some(config))
.expect("SA should succeed for Rastrigin");
assert!(
result.fun < 10.0,
"Should find good solution (fun = {})",
result.fun
);
}
#[test]
fn test_simulated_annealing_ackley() {
let ackley = |x: &[f64]| {
let (a, b, c) = (20.0, 0.2, 2.0 * std::f64::consts::PI);
let n = x.len() as f64;
let sum_sq = x.iter().map(|&xi| xi * xi).sum::<f64>();
let sum_cos = x.iter().map(|&xi| (c * xi).cos()).sum::<f64>();
-a * (-b * (sum_sq / n).sqrt()).exp() - (sum_cos / n).exp() + a + std::f64::consts::E
};
let config = SAConfig {
max_iter: 10000, initial_temp: 100.0, cooling_rate: 0.98, neighbor_sigma: 1.0, stagnation_limit: 500, ..Default::default()
};
let result = simulated_annealing(ackley, &[2.0, 2.0], Some(config))
.expect("SA should succeed for Ackley");
assert!(
result.fun < 10.0,
"Should find reasonable solution (fun = {})",
result.fun
);
}
#[test]
fn test_cooling_schedules() {
let f = |x: &[f64]| (x[0] - 1.0).powi(2) + (x[1] - 2.0).powi(2);
let exp_config = SAConfig {
max_iter: 1000,
cooling_schedule: CoolingSchedule::Exponential,
..Default::default()
};
let exp_result = simulated_annealing(f, &[0.0, 0.0], Some(exp_config))
.expect("SA with exponential cooling should succeed");
assert!(exp_result.fun < 1.0);
let log_config = SAConfig {
max_iter: 1000,
cooling_schedule: CoolingSchedule::Logarithmic,
initial_temp: 10.0,
..Default::default()
};
let log_result = simulated_annealing(f, &[0.0, 0.0], Some(log_config))
.expect("SA with logarithmic cooling should succeed");
assert!(log_result.fun < 1.0);
}
#[test]
fn test_neighbor_strategies() {
let f = |x: &[f64]| x.iter().map(|&xi| xi * xi).sum::<f64>();
let gaussian_config = SAConfig {
neighbor_strategy: NeighborStrategy::Gaussian,
neighbor_sigma: 1.0,
max_iter: 1000,
..Default::default()
};
let gauss_result = simulated_annealing(f, &[3.0, 3.0], Some(gaussian_config))
.expect("SA with Gaussian neighbor should succeed");
assert!(gauss_result.fun < 1.0);
let uniform_config = SAConfig {
neighbor_strategy: NeighborStrategy::Uniform,
neighbor_sigma: 1.0,
max_iter: 1000,
..Default::default()
};
let unif_result = simulated_annealing(f, &[3.0, 3.0], Some(uniform_config))
.expect("SA with uniform neighbor should succeed");
assert!(unif_result.fun < 1.0);
}
#[test]
fn test_rosenbrock() {
let rosenbrock = |x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2);
let config = SAConfig {
max_iter: 10000,
initial_temp: 100.0,
cooling_rate: 0.98,
neighbor_sigma: 0.5,
..Default::default()
};
let result = simulated_annealing(rosenbrock, &[0.0, 0.0], Some(config))
.expect("SA should succeed for Rosenbrock");
assert!(
result.fun < 10.0,
"Should find reasonable solution (fun = {})",
result.fun
);
}
}