use num_traits::Float;
use rand::Rng;
use std::fmt::Debug;
use crate::math::optimization::{ObjectiveFunction, OptimizationConfig, OptimizationResult};
#[derive(Debug, Clone)]
pub struct AnnealingConfig<T>
where
T: Float + Debug,
{
pub initial_temperature: T,
pub cooling_rate: T,
pub iterations_per_temp: usize,
pub lower_bounds: Vec<T>,
pub upper_bounds: Vec<T>,
}
impl<T> Default for AnnealingConfig<T>
where
T: Float + Debug,
{
fn default() -> Self {
Self {
initial_temperature: T::from(5.0).unwrap(), cooling_rate: T::from(0.98).unwrap(), iterations_per_temp: 150, lower_bounds: vec![T::from(-10.0).unwrap()],
upper_bounds: vec![T::from(10.0).unwrap()],
}
}
}
pub fn minimize<T, F>(
f: &F,
initial_point: &[T],
config: &OptimizationConfig<T>,
sa_config: &AnnealingConfig<T>,
) -> OptimizationResult<T>
where
T: Float + Debug,
F: ObjectiveFunction<T>,
{
let mut rng = rand::thread_rng();
let n = initial_point.len();
let mut current_point = initial_point.to_vec();
let mut current_value = f.evaluate(¤t_point);
let mut best_point = current_point.clone();
let mut best_value = current_value;
let mut temperature = sa_config.initial_temperature;
let mut iterations = 0;
let mut converged = false;
let mut no_improvement_count = 0;
let mut last_improvement_temp = temperature;
let min_temp = T::from(1e-10).unwrap();
let mut scale = T::one();
for i in 0..n {
let range = sa_config.upper_bounds[i.min(sa_config.upper_bounds.len() - 1)]
- sa_config.lower_bounds[i.min(sa_config.lower_bounds.len() - 1)];
scale = scale.max(range);
}
while iterations < config.max_iterations {
let mut improved = false;
let mut local_best_value = current_value;
for _ in 0..sa_config.iterations_per_temp {
let neighbor = generate_neighbor(
¤t_point,
temperature,
&sa_config.lower_bounds,
&sa_config.upper_bounds,
&mut rng,
);
let neighbor_value = f.evaluate(&neighbor);
if neighbor_value < local_best_value {
local_best_value = neighbor_value;
}
let delta = neighbor_value - current_value;
let accept = if delta <= T::zero() {
true
} else {
let scale = (current_value.abs() + T::one()).max(T::from(1e-8).unwrap());
let scaled_delta = delta / scale;
let probability = (-scaled_delta / (temperature.max(min_temp)))
.exp()
.to_f64()
.unwrap();
probability > rng.gen::<f64>()
};
if accept {
current_point = neighbor;
current_value = neighbor_value;
if current_value < best_value {
best_point = current_point.clone();
best_value = current_value;
improved = true;
no_improvement_count = 0;
last_improvement_temp = temperature;
}
}
}
if !improved {
no_improvement_count += 1;
}
let temp_criterion = temperature < config.tolerance;
let improvement_criterion = no_improvement_count >= 10;
let value_criterion = best_value.abs() < config.tolerance;
let progress_criterion =
(local_best_value - best_value).abs() < config.tolerance * best_value.abs();
if (temp_criterion && progress_criterion) || improvement_criterion || value_criterion {
converged = true;
break;
}
let cooling_factor = if improved {
sa_config.cooling_rate
} else if temperature > last_improvement_temp * T::from(0.1).unwrap() {
sa_config.cooling_rate * T::from(0.9).unwrap()
} else {
sa_config.cooling_rate.sqrt()
};
temperature = temperature * cooling_factor;
iterations += 1;
}
OptimizationResult {
optimal_point: best_point,
optimal_value: best_value,
iterations,
converged,
}
}
fn generate_neighbor<T, R: Rng>(
point: &[T],
temperature: T,
lower_bounds: &[T],
upper_bounds: &[T],
rng: &mut R,
) -> Vec<T>
where
T: Float + Debug,
{
let n = point.len();
let mut neighbor = Vec::with_capacity(n);
let temp_factor = temperature.to_f64().unwrap().max(1e-10);
let base_step = if temp_factor < 0.1 { 0.01 } else { 0.02 };
for i in 0..n {
let lower = lower_bounds[i.min(lower_bounds.len() - 1)]
.to_f64()
.unwrap();
let upper = upper_bounds[i.min(upper_bounds.len() - 1)]
.to_f64()
.unwrap();
let current = point[i].to_f64().unwrap();
let range = (upper - lower) * base_step;
let step_size = (range * temp_factor.powf(0.5)).max(1e-10);
let use_cauchy = rng.gen::<f64>() < temp_factor.min(0.3); let perturbation = if use_cauchy {
let u1 = rng.gen::<f64>();
let u2 = rng.gen::<f64>();
step_size * (std::f64::consts::PI * (u1 - 0.5)).tan() * u2
} else {
let u1 = rng.gen::<f64>();
let u2 = rng.gen::<f64>();
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f64::consts::PI * u2;
step_size * r * theta.cos() * 0.5 };
let mut new_value = current + perturbation;
if new_value < lower {
new_value = lower + (lower - new_value).abs() % ((upper - lower) * 0.05);
}
if new_value > upper {
new_value = upper - (new_value - upper).abs() % ((upper - lower) * 0.05);
}
neighbor.push(T::from(new_value).unwrap());
}
neighbor
}
#[cfg(test)]
mod tests {
use super::*;
struct Quadratic;
impl ObjectiveFunction<f64> for Quadratic {
fn evaluate(&self, point: &[f64]) -> f64 {
point.iter().map(|x| x * x).sum()
}
}
#[test]
fn test_simulated_annealing_quadratic() {
let f = Quadratic;
let initial_point = vec![1.0, 1.0];
let config = OptimizationConfig {
max_iterations: 500,
tolerance: 1e-4,
learning_rate: 1.0,
};
let sa_config = AnnealingConfig {
initial_temperature: 5.0,
cooling_rate: 0.99,
iterations_per_temp: 200,
lower_bounds: vec![-2.0, -2.0],
upper_bounds: vec![2.0, 2.0],
};
const NUM_TRIALS: usize = 5;
let mut successful_trials = 0;
for _ in 0..NUM_TRIALS {
let result = minimize(&f, &initial_point, &config, &sa_config);
if result.converged && result.optimal_value < 0.1 {
successful_trials += 1;
}
}
let success_rate = successful_trials as f64 / NUM_TRIALS as f64;
assert!(
success_rate >= 0.6,
"Success rate {:.2} below required threshold of 0.6 ({} out of {})",
success_rate,
successful_trials,
NUM_TRIALS
);
}
struct QuadraticWithMinimum;
impl ObjectiveFunction<f64> for QuadraticWithMinimum {
fn evaluate(&self, point: &[f64]) -> f64 {
let x = point[0];
(x - 2.0).powi(2)
}
}
#[test]
fn test_simulated_annealing_quadratic_with_minimum() {
let f = QuadraticWithMinimum;
let initial_point = vec![0.0];
let config = OptimizationConfig {
max_iterations: 200, tolerance: 1e-4, learning_rate: 1.0,
};
let sa_config = AnnealingConfig {
initial_temperature: 2.0, cooling_rate: 0.98, iterations_per_temp: 150, lower_bounds: vec![-5.0], upper_bounds: vec![5.0],
};
let result = minimize(&f, &initial_point, &config, &sa_config);
assert!(result.converged);
assert!((result.optimal_point[0] - 2.0).abs() < 1e-2);
assert!(result.optimal_value < 1e-4);
}
struct Rosenbrock;
impl ObjectiveFunction<f64> for Rosenbrock {
fn evaluate(&self, point: &[f64]) -> f64 {
let x = point[0];
let y = point[1];
(x - 1.0).powi(2) + 100.0 * (y - x.powi(2)).powi(2)
}
}
#[test]
fn test_simulated_annealing_rosenbrock() {
let f = Rosenbrock;
let initial_point = vec![0.0, 0.0];
let config = OptimizationConfig {
max_iterations: 1000,
tolerance: 1e-4,
learning_rate: 1.0,
};
let sa_config = AnnealingConfig {
initial_temperature: 10.0,
cooling_rate: 0.99,
iterations_per_temp: 200,
lower_bounds: vec![-10.0, -10.0],
upper_bounds: vec![10.0, 10.0],
};
const NUM_TRIALS: usize = 10;
let mut successful_trials = 0;
for _ in 0..NUM_TRIALS {
let result = minimize(&f, &initial_point, &config, &sa_config);
if result.converged && result.optimal_value < 5.0 {
successful_trials += 1;
}
}
let success_rate = successful_trials as f64 / NUM_TRIALS as f64;
assert!(
success_rate >= 0.6,
"Success rate {:.2} below required threshold of 0.6 ({} out of {})",
success_rate,
successful_trials,
NUM_TRIALS
);
}
struct MultiModal;
impl ObjectiveFunction<f64> for MultiModal {
fn evaluate(&self, point: &[f64]) -> f64 {
let x = point[0];
let y = point[1];
(x.powi(2) + y - 11.0).powi(2) + (x + y.powi(2) - 7.0).powi(2)
}
}
#[test]
fn test_simulated_annealing_multimodal() {
let f = MultiModal;
let initial_point = vec![0.0, 0.0];
let config = OptimizationConfig {
max_iterations: 300, tolerance: 1e-4, learning_rate: 1.0,
};
let sa_config = AnnealingConfig {
initial_temperature: 10.0, cooling_rate: 0.98, iterations_per_temp: 200, lower_bounds: vec![-5.0, -5.0], upper_bounds: vec![5.0, 5.0],
};
let result = minimize(&f, &initial_point, &config, &sa_config);
assert!(result.converged);
assert!(result.optimal_value < 1.0);
}
}