use crate::error::OptimizeError;
use crate::unconstrained::Options;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct AdaptiveToleranceOptions {
pub initial_ftol: f64,
pub initial_gtol: f64,
pub initial_xtol: f64,
pub min_tolerance: f64,
pub max_tolerance: f64,
pub adaptation_rate: f64,
pub history_length: usize,
pub use_function_scale: bool,
pub use_gradient_scale: bool,
pub use_stagnation_detection: bool,
pub use_problem_conditioning: bool,
}
impl Default for AdaptiveToleranceOptions {
fn default() -> Self {
Self {
initial_ftol: 1e-8,
initial_gtol: 1e-5,
initial_xtol: 1e-8,
min_tolerance: 1e-15,
max_tolerance: 1e-3,
adaptation_rate: 0.5,
history_length: 10,
use_function_scale: true,
use_gradient_scale: true,
use_stagnation_detection: true,
use_problem_conditioning: false, }
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveToleranceState {
pub current_ftol: f64,
pub current_gtol: f64,
pub current_xtol: f64,
function_history: VecDeque<f64>,
gradient_history: VecDeque<f64>,
step_history: VecDeque<f64>,
function_scale: f64,
gradient_scale: f64,
stagnant_nit: usize,
problem_dim: usize,
options: AdaptiveToleranceOptions,
}
impl AdaptiveToleranceState {
pub fn new(options: AdaptiveToleranceOptions, problem_dim: usize) -> Self {
Self {
current_ftol: options.initial_ftol,
current_gtol: options.initial_gtol,
current_xtol: options.initial_xtol,
function_history: VecDeque::with_capacity(options.history_length),
gradient_history: VecDeque::with_capacity(options.history_length),
step_history: VecDeque::with_capacity(options.history_length),
function_scale: 1.0,
gradient_scale: 1.0,
stagnant_nit: 0,
problem_dim,
options,
}
}
pub fn update(
&mut self,
function_value: f64,
gradient_norm: f64,
step_norm: f64,
iteration: usize,
) -> Result<(), OptimizeError> {
self.add_to_history(function_value, gradient_norm, step_norm);
if self.options.use_function_scale {
self.update_function_scale();
}
if self.options.use_gradient_scale {
self.update_gradient_scale();
}
if self.options.use_stagnation_detection {
self.update_stagnation_detection();
}
self.adapt_tolerances(iteration)?;
Ok(())
}
fn add_to_history(&mut self, function_value: f64, gradient_norm: f64, step_norm: f64) {
if self.function_history.len() >= self.options.history_length {
self.function_history.pop_front();
}
self.function_history.push_back(function_value);
if self.gradient_history.len() >= self.options.history_length {
self.gradient_history.pop_front();
}
self.gradient_history.push_back(gradient_norm);
if self.step_history.len() >= self.options.history_length {
self.step_history.pop_front();
}
self.step_history.push_back(step_norm);
}
fn update_function_scale(&mut self) {
if self.function_history.len() < 2 {
return;
}
let mut values: Vec<f64> = self.function_history.iter().map(|&x| x.abs()).collect();
values.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
let median_idx = values.len() / 2;
let scale = if values.len().is_multiple_of(2) {
0.5 * (values[median_idx - 1] + values[median_idx])
} else {
values[median_idx]
};
let alpha = 0.1; self.function_scale = alpha * scale.max(1e-15) + (1.0 - alpha) * self.function_scale;
}
fn update_gradient_scale(&mut self) {
if self.gradient_history.len() < 2 {
return;
}
let mut values: Vec<f64> = self.gradient_history.iter().cloned().collect();
values.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
let median_idx = values.len() / 2;
let scale = if values.len().is_multiple_of(2) {
0.5 * (values[median_idx - 1] + values[median_idx])
} else {
values[median_idx]
};
let alpha = 0.1;
self.gradient_scale = alpha * scale.max(1e-15) + (1.0 - alpha) * self.gradient_scale;
}
fn update_stagnation_detection(&mut self) {
if self.function_history.len() < 3 {
self.stagnant_nit = 0;
return;
}
let recent_len = std::cmp::min(3, self.function_history.len());
let recent_values: Vec<f64> = self
.function_history
.iter()
.rev()
.take(recent_len)
.cloned()
.collect();
let relative_change = if recent_values.len() >= 2 {
let f_new = recent_values[0];
let f_old = recent_values[recent_values.len() - 1];
if f_old.abs() > 1e-15 {
(f_old - f_new).abs() / f_old.abs()
} else {
(f_old - f_new).abs()
}
} else {
1.0
};
if relative_change < 1e-10 {
self.stagnant_nit += 1;
} else {
self.stagnant_nit = 0;
}
}
fn adapt_tolerances(&mut self, iteration: usize) -> Result<(), OptimizeError> {
if iteration < 5 || self.options.adaptation_rate <= 0.0 {
return Ok(()); }
if self.options.use_function_scale {
let scale_factor = self.function_scale;
let adaptive_ftol = self.options.initial_ftol * scale_factor;
self.current_ftol = self.blend_tolerance(
self.current_ftol,
adaptive_ftol,
self.options.adaptation_rate * 0.5, );
}
if self.options.use_gradient_scale {
let scale_factor = self.gradient_scale;
let dimension_factor = (self.problem_dim as f64).sqrt(); let adaptive_gtol = self.options.initial_gtol * scale_factor / dimension_factor;
self.current_gtol = self.blend_tolerance(
self.current_gtol,
adaptive_gtol,
self.options.adaptation_rate,
);
}
let adaptive_xtol = if self.step_history.len() >= 3 {
let typical_step =
self.step_history.iter().sum::<f64>() / self.step_history.len() as f64;
self.options.initial_xtol * typical_step.max(1e-12)
} else {
self.options.initial_xtol
};
self.current_xtol = self.blend_tolerance(
self.current_xtol,
adaptive_xtol,
self.options.adaptation_rate * 0.3, );
if self.options.use_stagnation_detection && self.stagnant_nit > 5 {
let relaxation_factor = 1.0 + 0.1 * (self.stagnant_nit as f64 - 5.0).min(10.0);
self.current_ftol *= relaxation_factor;
self.current_gtol *= relaxation_factor;
self.current_xtol *= relaxation_factor;
}
self.current_ftol = self
.current_ftol
.clamp(self.options.min_tolerance, self.options.max_tolerance);
self.current_gtol = self
.current_gtol
.clamp(self.options.min_tolerance, self.options.max_tolerance);
self.current_xtol = self
.current_xtol
.clamp(self.options.min_tolerance, self.options.max_tolerance);
Ok(())
}
fn blend_tolerance(&self, current: f64, adaptive: f64, rate: f64) -> f64 {
rate * adaptive + (1.0 - rate) * current
}
pub fn get_current_options(&self, base_options: &Options) -> Options {
let mut options = base_options.clone();
options.ftol = self.current_ftol;
options.gtol = self.current_gtol;
options.xtol = self.current_xtol;
options
}
pub fn check_convergence(
&self,
function_change: f64,
step_norm: f64,
gradient_norm: f64,
) -> ConvergenceStatus {
let f_converged = function_change.abs() < self.current_ftol;
let x_converged = step_norm < self.current_xtol;
let g_converged = gradient_norm < self.current_gtol;
ConvergenceStatus {
f_converged,
x_converged,
g_converged,
overall_converged: f_converged || x_converged || g_converged,
ftol_used: self.current_ftol,
xtol_used: self.current_xtol,
gtol_used: self.current_gtol,
}
}
pub fn get_adaptation_stats(&self) -> AdaptationStats {
AdaptationStats {
function_scale: self.function_scale,
gradient_scale: self.gradient_scale,
stagnant_nit: self.stagnant_nit,
current_ftol: self.current_ftol,
current_gtol: self.current_gtol,
current_xtol: self.current_xtol,
history_length: self.function_history.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct ConvergenceStatus {
pub f_converged: bool,
pub x_converged: bool,
pub g_converged: bool,
pub overall_converged: bool,
pub ftol_used: f64,
pub xtol_used: f64,
pub gtol_used: f64,
}
#[derive(Debug, Clone)]
pub struct AdaptationStats {
pub function_scale: f64,
pub gradient_scale: f64,
pub stagnant_nit: usize,
pub current_ftol: f64,
pub current_gtol: f64,
pub current_xtol: f64,
pub history_length: usize,
}
#[allow(dead_code)]
pub fn create_adaptive_options_for_problem(
problem_type: &str,
problem_size: usize,
) -> AdaptiveToleranceOptions {
match problem_type.to_lowercase().as_str() {
"well_conditioned" => AdaptiveToleranceOptions {
initial_ftol: 1e-10,
initial_gtol: 1e-7,
initial_xtol: 1e-10,
adaptation_rate: 0.3,
use_function_scale: true,
use_gradient_scale: true,
use_stagnation_detection: false, ..Default::default()
},
"ill_conditioned" => AdaptiveToleranceOptions {
initial_ftol: 1e-6,
initial_gtol: 1e-4,
initial_xtol: 1e-6,
adaptation_rate: 0.7,
use_function_scale: true,
use_gradient_scale: true,
use_stagnation_detection: true,
use_problem_conditioning: true,
..Default::default()
},
"noisy" => AdaptiveToleranceOptions {
initial_ftol: 1e-5,
initial_gtol: 1e-3,
initial_xtol: 1e-5,
adaptation_rate: 0.8,
history_length: 20, use_stagnation_detection: true,
..Default::default()
},
"high_dimensional" => {
let dimension_factor = (problem_size as f64).sqrt();
AdaptiveToleranceOptions {
initial_ftol: 1e-8,
initial_gtol: 1e-5 * dimension_factor,
initial_xtol: 1e-8,
adaptation_rate: 0.6,
use_gradient_scale: true,
..Default::default()
}
}
_ => AdaptiveToleranceOptions::default(),
}
}
#[allow(dead_code)]
pub fn check_convergence_adaptive(
function_change: f64,
step_norm: f64,
gradient_norm: f64,
adaptive_state: &AdaptiveToleranceState,
) -> ConvergenceStatus {
adaptive_state.check_convergence(function_change, step_norm, gradient_norm)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adaptive_tolerance_initialization() {
let options = AdaptiveToleranceOptions::default();
let state = AdaptiveToleranceState::new(options.clone(), 10);
assert_eq!(state.current_ftol, options.initial_ftol);
assert_eq!(state.current_gtol, options.initial_gtol);
assert_eq!(state.current_xtol, options.initial_xtol);
assert_eq!(state.problem_dim, 10);
}
#[test]
fn test_scale_adaptation() {
let options = AdaptiveToleranceOptions {
use_function_scale: true,
adaptation_rate: 1.0, ..Default::default()
};
let mut state = AdaptiveToleranceState::new(options, 5);
for i in 0..10 {
let f_val = 100.0 + i as f64; state
.update(f_val, 1e-5, 1e-8, i)
.expect("Operation failed");
}
let stats = state.get_adaptation_stats();
assert!(stats.function_scale > 50.0 && stats.function_scale < 200.0);
}
#[test]
fn test_stagnation_detection() {
let options = AdaptiveToleranceOptions {
use_stagnation_detection: true,
..Default::default()
};
let mut state = AdaptiveToleranceState::new(options, 5);
for i in 0..10 {
state.update(1.0, 1e-5, 1e-8, i).expect("Operation failed");
}
let stats = state.get_adaptation_stats();
assert!(stats.stagnant_nit > 5);
}
#[test]
fn test_convergence_check() {
let options = AdaptiveToleranceOptions::default();
let state = AdaptiveToleranceState::new(options, 5);
let status = state.check_convergence(1e-10, 1e-10, 1e-10);
assert!(status.overall_converged);
assert!(status.f_converged);
assert!(status.x_converged);
assert!(status.g_converged);
}
#[test]
fn test_problem_specific_options() {
let well_cond = create_adaptive_options_for_problem("well_conditioned", 10);
assert!(well_cond.initial_ftol < 1e-8);
assert!(!well_cond.use_stagnation_detection);
let ill_cond = create_adaptive_options_for_problem("ill_conditioned", 10);
assert!(ill_cond.adaptation_rate > 0.5);
assert!(ill_cond.use_stagnation_detection);
let high_dim = create_adaptive_options_for_problem("high_dimensional", 1000);
assert!(high_dim.initial_gtol > 1e-5); }
#[test]
fn test_tolerance_bounds() {
let options = AdaptiveToleranceOptions {
min_tolerance: 1e-12,
max_tolerance: 1e-4,
adaptation_rate: 1.0, ..Default::default()
};
let mut state = AdaptiveToleranceState::new(options, 5);
for i in 0..10 {
state.update(1e10, 1e10, 1e10, i).expect("Operation failed");
}
assert!(state.current_ftol >= 1e-12);
assert!(state.current_ftol <= 1e-4);
assert!(state.current_gtol >= 1e-12);
assert!(state.current_gtol <= 1e-4);
}
}