1use crate::error::OptimizeError;
11use crate::unconstrained::adaptive_convergence::{
12 AdaptiveToleranceOptions, AdaptiveToleranceState, ConvergenceStatus,
13};
14use scirs2_core::ndarray::ArrayView1;
15use std::collections::VecDeque;
16use std::time::{Duration, Instant};
17
18#[derive(Debug, Clone)]
20pub struct RobustConvergenceOptions {
21 pub adaptive_tolerance: AdaptiveToleranceOptions,
23 pub enable_early_stopping: bool,
25 pub early_stopping_patience: usize,
27 pub early_stopping_min_delta: f64,
29 pub enable_progress_based: bool,
31 pub progress_window: usize,
33 pub min_progress_rate: f64,
35 pub enable_time_limit: bool,
37 pub max_time: Duration,
39 pub enable_noise_robust: bool,
41 pub noise_window: usize,
43 pub noise_confidence: f64,
45 pub require_multiple_criteria: bool,
47 pub min_criteria_count: usize,
49 pub enable_plateau_detection: bool,
51 pub plateau_tolerance: f64,
53 pub plateau_window: usize,
55}
56
57impl Default for RobustConvergenceOptions {
58 fn default() -> Self {
59 Self {
60 adaptive_tolerance: AdaptiveToleranceOptions::default(),
61 enable_early_stopping: true,
62 early_stopping_patience: 20,
63 early_stopping_min_delta: 1e-8,
64 enable_progress_based: true,
65 progress_window: 10,
66 min_progress_rate: 1e-10,
67 enable_time_limit: false,
68 max_time: Duration::from_secs(3600), enable_noise_robust: true,
70 noise_window: 5,
71 noise_confidence: 0.95,
72 require_multiple_criteria: false,
73 min_criteria_count: 2,
74 enable_plateau_detection: true,
75 plateau_tolerance: 1e-12,
76 plateau_window: 15,
77 }
78 }
79}
80
81#[derive(Debug)]
83pub struct RobustConvergenceState {
84 adaptive_state: AdaptiveToleranceState,
86 early_stop_state: EarlyStoppingState,
88 progress_state: ProgressState,
90 start_time: Option<Instant>,
92 noise_state: NoiseRobustState,
94 plateau_state: PlateauState,
96 options: RobustConvergenceOptions,
98}
99
100#[derive(Debug, Clone)]
102struct EarlyStoppingState {
103 best_value: f64,
104 iterations_without_improvement: usize,
105 improvement_history: VecDeque<f64>,
106}
107
108#[derive(Debug, Clone)]
110struct ProgressState {
111 recent_values: VecDeque<f64>,
112 recent_gradients: VecDeque<f64>,
113 progress_rate: f64,
114}
115
116#[derive(Debug, Clone)]
118struct NoiseRobustState {
119 function_window: VecDeque<f64>,
120 gradient_window: VecDeque<f64>,
121 step_window: VecDeque<f64>,
122 #[allow(dead_code)]
123 stable_convergence_count: usize,
124}
125
126#[derive(Debug, Clone)]
128struct PlateauState {
129 plateau_window: VecDeque<f64>,
130 plateau_detected: bool,
131 plateau_start_iteration: Option<usize>,
132}
133
134impl RobustConvergenceState {
135 pub fn new(options: RobustConvergenceOptions, problem_dim: usize) -> Self {
137 let adaptive_state =
138 AdaptiveToleranceState::new(options.adaptive_tolerance.clone(), problem_dim);
139
140 Self {
141 adaptive_state,
142 early_stop_state: EarlyStoppingState {
143 best_value: f64::INFINITY,
144 iterations_without_improvement: 0,
145 improvement_history: VecDeque::with_capacity(options.early_stopping_patience),
146 },
147 progress_state: ProgressState {
148 recent_values: VecDeque::with_capacity(options.progress_window),
149 recent_gradients: VecDeque::with_capacity(options.progress_window),
150 progress_rate: 0.0,
151 },
152 start_time: Some(std::time::Instant::now()),
153 noise_state: NoiseRobustState {
154 function_window: VecDeque::with_capacity(options.noise_window),
155 gradient_window: VecDeque::with_capacity(options.noise_window),
156 step_window: VecDeque::with_capacity(options.noise_window),
157 stable_convergence_count: 0,
158 },
159 plateau_state: PlateauState {
160 plateau_window: VecDeque::with_capacity(options.plateau_window),
161 plateau_detected: false,
162 plateau_start_iteration: None,
163 },
164 options,
165 }
166 }
167
168 pub fn start_timing(&mut self) {
170 if self.options.enable_time_limit {
171 self.start_time = Some(Instant::now());
172 }
173 }
174
175 pub fn update_and_check_convergence(
177 &mut self,
178 function_value: f64,
179 gradient_norm: f64,
180 step_norm: f64,
181 iteration: usize,
182 x: Option<&ArrayView1<f64>>,
183 ) -> Result<RobustConvergenceResult, OptimizeError> {
184 self.adaptive_state
186 .update(function_value, gradient_norm, step_norm, iteration)?;
187
188 if self.options.enable_early_stopping {
190 self.update_early_stopping(function_value, iteration);
191 }
192
193 if self.options.enable_progress_based {
195 self.update_progress(function_value, gradient_norm);
196 }
197
198 if self.options.enable_noise_robust {
200 self.update_noise_robust(function_value, gradient_norm, step_norm);
201 }
202
203 if self.options.enable_plateau_detection {
205 self.update_plateau_detection(function_value, iteration);
206 }
207
208 self.check_all_convergence_criteria(function_value, gradient_norm, step_norm, iteration)
210 }
211
212 fn update_early_stopping(&mut self, function_value: f64, iteration: usize) {
214 let improvement = self.early_stop_state.best_value - function_value;
215
216 if improvement > self.options.early_stopping_min_delta {
217 self.early_stop_state.best_value = function_value;
218 self.early_stop_state.iterations_without_improvement = 0;
219 } else {
220 self.early_stop_state.iterations_without_improvement += 1;
221 }
222
223 if self.early_stop_state.improvement_history.len() >= self.options.early_stopping_patience {
225 self.early_stop_state.improvement_history.pop_front();
226 }
227 self.early_stop_state
228 .improvement_history
229 .push_back(improvement);
230 }
231
232 fn update_progress(&mut self, function_value: f64, gradient_norm: f64) {
234 if self.progress_state.recent_values.len() >= self.options.progress_window {
236 self.progress_state.recent_values.pop_front();
237 }
238 self.progress_state.recent_values.push_back(function_value);
239
240 if self.progress_state.recent_gradients.len() >= self.options.progress_window {
241 self.progress_state.recent_gradients.pop_front();
242 }
243 self.progress_state
244 .recent_gradients
245 .push_back(gradient_norm);
246
247 if self.progress_state.recent_values.len() >= 2 {
249 let values: Vec<f64> = self.progress_state.recent_values.iter().cloned().collect();
250 let n = values.len();
251
252 let x_mean = (n - 1) as f64 / 2.0;
254 let y_mean = values.iter().sum::<f64>() / n as f64;
255
256 let mut num = 0.0;
257 let mut den = 0.0;
258
259 for (i, &y) in values.iter().enumerate() {
260 let x = i as f64;
261 num += (x - x_mean) * (y - y_mean);
262 den += (x - x_mean).powi(2);
263 }
264
265 self.progress_state.progress_rate = if den > 1e-15 { -num / den } else { 0.0 };
266 }
267 }
268
269 fn update_noise_robust(&mut self, function_value: f64, gradient_norm: f64, step_norm: f64) {
271 if self.noise_state.function_window.len() >= self.options.noise_window {
273 self.noise_state.function_window.pop_front();
274 }
275 self.noise_state.function_window.push_back(function_value);
276
277 if self.noise_state.gradient_window.len() >= self.options.noise_window {
278 self.noise_state.gradient_window.pop_front();
279 }
280 self.noise_state.gradient_window.push_back(gradient_norm);
281
282 if self.noise_state.step_window.len() >= self.options.noise_window {
283 self.noise_state.step_window.pop_front();
284 }
285 self.noise_state.step_window.push_back(step_norm);
286 }
287
288 fn update_plateau_detection(&mut self, function_value: f64, iteration: usize) {
290 if self.plateau_state.plateau_window.len() >= self.options.plateau_window {
291 self.plateau_state.plateau_window.pop_front();
292 }
293 self.plateau_state.plateau_window.push_back(function_value);
294
295 if self.plateau_state.plateau_window.len() >= self.options.plateau_window {
297 let values: Vec<f64> = self.plateau_state.plateau_window.iter().cloned().collect();
298 let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
299 let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
300
301 if (max_val - min_val).abs() < self.options.plateau_tolerance {
302 if !self.plateau_state.plateau_detected {
303 self.plateau_state.plateau_detected = true;
304 self.plateau_state.plateau_start_iteration = Some(iteration);
305 }
306 } else {
307 self.plateau_state.plateau_detected = false;
308 self.plateau_state.plateau_start_iteration = None;
309 }
310 }
311 }
312
313 fn check_all_convergence_criteria(
315 &self,
316 function_value: f64,
317 gradient_norm: f64,
318 step_norm: f64,
319 _iteration: usize,
320 ) -> Result<RobustConvergenceResult, OptimizeError> {
321 let mut convergence_reasons = Vec::new();
322 let mut warning_flags = Vec::new();
323
324 let function_change = if self.progress_state.recent_values.len() >= 2 {
326 let recent: Vec<f64> = self.progress_state.recent_values.iter().cloned().collect();
327 recent[recent.len() - 1] - recent[recent.len() - 2]
328 } else {
329 0.0
330 };
331
332 let adaptive_status =
334 self.adaptive_state
335 .check_convergence(function_change, step_norm, gradient_norm);
336
337 let mut criteria_met = 0;
338
339 if adaptive_status.f_converged {
341 convergence_reasons.push("Function tolerance".to_string());
342 criteria_met += 1;
343 }
344 if adaptive_status.x_converged {
345 convergence_reasons.push("Step tolerance".to_string());
346 criteria_met += 1;
347 }
348 if adaptive_status.g_converged {
349 convergence_reasons.push("Gradient tolerance".to_string());
350 criteria_met += 1;
351 }
352
353 if self.options.enable_early_stopping {
355 let early_stop_triggered = self.early_stop_state.iterations_without_improvement
356 >= self.options.early_stopping_patience;
357
358 if early_stop_triggered {
359 convergence_reasons.push("Early stopping".to_string());
360 criteria_met += 1;
361 }
362 }
363
364 if self.options.enable_progress_based
366 && self.progress_state.progress_rate.abs() < self.options.min_progress_rate
367 {
368 convergence_reasons.push("Insufficient progress".to_string());
369 criteria_met += 1;
370 }
371
372 if self.options.enable_time_limit {
374 if let Some(start_time) = self.start_time {
375 if start_time.elapsed() > self.options.max_time {
376 convergence_reasons.push("Time limit exceeded".to_string());
377 criteria_met += 1;
378 }
379 }
380 }
381
382 if self.options.enable_noise_robust {
384 let noise_converged = self.check_noise_robust_convergence();
385 if noise_converged {
386 convergence_reasons.push("Noise-robust convergence".to_string());
387 criteria_met += 1;
388 }
389 }
390
391 if self.options.enable_plateau_detection && self.plateau_state.plateau_detected {
393 convergence_reasons.push("Plateau detected".to_string());
394 criteria_met += 1;
395 }
396
397 let converged = if self.options.require_multiple_criteria {
399 criteria_met >= self.options.min_criteria_count
400 } else {
401 criteria_met > 0
402 };
403
404 if self.early_stop_state.iterations_without_improvement
406 > self.options.early_stopping_patience / 2
407 {
408 warning_flags.push("Slow convergence detected".to_string());
409 }
410
411 if gradient_norm > 1e3 {
412 warning_flags.push("Large gradient _norm".to_string());
413 }
414
415 if self.plateau_state.plateau_detected {
416 warning_flags.push("Function plateau detected".to_string());
417 }
418
419 Ok(RobustConvergenceResult {
420 converged,
421 convergence_reasons,
422 warning_flags,
423 adaptive_status,
424 criteria_met,
425 early_stopping_nit: self.early_stop_state.iterations_without_improvement,
426 progress_rate: self.progress_state.progress_rate,
427 time_elapsed: self.start_time.map(|t| t.elapsed()),
428 plateau_detected: self.plateau_state.plateau_detected,
429 noise_robust_confidence: self.calculate_noise_confidence(),
430 })
431 }
432
433 fn check_noise_robust_convergence(&self) -> bool {
435 if self.noise_state.function_window.len() < self.options.noise_window {
436 return false;
437 }
438
439 let f_values: Vec<f64> = self.noise_state.function_window.iter().cloned().collect();
441 let g_values: Vec<f64> = self.noise_state.gradient_window.iter().cloned().collect();
442
443 let f_mean = f_values.iter().sum::<f64>() / f_values.len() as f64;
445 let f_std = (f_values.iter().map(|x| (x - f_mean).powi(2)).sum::<f64>()
446 / f_values.len() as f64)
447 .sqrt();
448
449 let g_mean = g_values.iter().sum::<f64>() / g_values.len() as f64;
450 let g_std = (g_values.iter().map(|x| (x - g_mean).powi(2)).sum::<f64>()
451 / g_values.len() as f64)
452 .sqrt();
453
454 let f_margin = f_std * 1.96; let g_margin = g_std * 1.96;
457
458 let f_converged = f_mean.abs() < self.adaptive_state.current_ftol + f_margin;
459 let g_converged = g_mean < self.adaptive_state.current_gtol + g_margin;
460
461 f_converged && g_converged
462 }
463
464 fn calculate_noise_confidence(&self) -> f64 {
466 if self.noise_state.function_window.len() < 3 {
467 return 0.0;
468 }
469
470 let values: Vec<f64> = self.noise_state.function_window.iter().cloned().collect();
471 let mean = values.iter().sum::<f64>() / values.len() as f64;
472 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
473
474 let cv = if mean.abs() > 1e-15 {
476 variance.sqrt() / mean.abs()
477 } else {
478 f64::INFINITY
479 };
480
481 (1.0 / (1.0 + cv)).clamp(0.0, 1.0)
483 }
484
485 pub fn get_adaptive_state(&self) -> &AdaptiveToleranceState {
487 &self.adaptive_state
488 }
489}
490
491#[derive(Debug, Clone)]
493pub struct RobustConvergenceResult {
494 pub converged: bool,
496 pub convergence_reasons: Vec<String>,
498 pub warning_flags: Vec<String>,
500 pub adaptive_status: ConvergenceStatus,
502 pub criteria_met: usize,
504 pub early_stopping_nit: usize,
506 pub progress_rate: f64,
508 pub time_elapsed: Option<Duration>,
510 pub plateau_detected: bool,
512 pub noise_robust_confidence: f64,
514}
515
516impl RobustConvergenceResult {
517 pub fn get_message(&self) -> String {
519 if self.converged {
520 if self.convergence_reasons.is_empty() {
521 "Optimization converged".to_string()
522 } else {
523 format!(
524 "Optimization converged: {}",
525 self.convergence_reasons.join(", ")
526 )
527 }
528 } else {
529 "Optimization has not converged".to_string()
530 }
531 }
532
533 pub fn get_detailed_report(&self) -> String {
535 let mut report = String::new();
536
537 report.push_str(&format!(
538 "Convergence Status: {}\n",
539 if self.converged {
540 "CONVERGED"
541 } else {
542 "NOT CONVERGED"
543 }
544 ));
545
546 if !self.convergence_reasons.is_empty() {
547 report.push_str(&format!(
548 "Convergence reasons: {}\n",
549 self.convergence_reasons.join(", ")
550 ));
551 }
552
553 if !self.warning_flags.is_empty() {
554 report.push_str(&format!("Warnings: {}\n", self.warning_flags.join(", ")));
555 }
556
557 report.push_str(&format!("Criteria met: {}\n", self.criteria_met));
558 report.push_str(&format!("Progress rate: {:.3e}\n", self.progress_rate));
559
560 if let Some(time) = self.time_elapsed {
561 report.push_str(&format!(
562 "Time elapsed: {:.3} seconds\n",
563 time.as_secs_f64()
564 ));
565 }
566
567 if self.noise_robust_confidence > 0.0 {
568 report.push_str(&format!(
569 "Noise confidence: {:.1}%\n",
570 self.noise_robust_confidence * 100.0
571 ));
572 }
573
574 report.push_str(&format!(
575 "Function tolerance: {:.3e}\n",
576 self.adaptive_status.ftol_used
577 ));
578 report.push_str(&format!(
579 "Gradient tolerance: {:.3e}\n",
580 self.adaptive_status.gtol_used
581 ));
582 report.push_str(&format!(
583 "Step tolerance: {:.3e}\n",
584 self.adaptive_status.xtol_used
585 ));
586
587 report
588 }
589}
590
591#[allow(dead_code)]
593pub fn create_robust_options_for_problem(
594 problem_type: &str,
595 problem_size: usize,
596 expected_difficulty: &str,
597) -> RobustConvergenceOptions {
598 let adaptive_tolerance =
599 crate::unconstrained::adaptive_convergence::create_adaptive_options_for_problem(
600 problem_type,
601 problem_size,
602 );
603
604 match expected_difficulty.to_lowercase().as_str() {
605 "easy" => RobustConvergenceOptions {
606 adaptive_tolerance,
607 enable_early_stopping: false,
608 enable_progress_based: false,
609 enable_noise_robust: false,
610 require_multiple_criteria: false,
611 ..Default::default()
612 },
613 "moderate" => RobustConvergenceOptions {
614 adaptive_tolerance,
615 enable_early_stopping: true,
616 early_stopping_patience: 15,
617 enable_progress_based: true,
618 enable_noise_robust: true,
619 ..Default::default()
620 },
621 "difficult" => RobustConvergenceOptions {
622 adaptive_tolerance,
623 enable_early_stopping: true,
624 early_stopping_patience: 30,
625 enable_progress_based: true,
626 enable_noise_robust: true,
627 require_multiple_criteria: true,
628 min_criteria_count: 2,
629 enable_plateau_detection: true,
630 ..Default::default()
631 },
632 "very_difficult" => RobustConvergenceOptions {
633 adaptive_tolerance,
634 enable_early_stopping: true,
635 early_stopping_patience: 50,
636 enable_progress_based: true,
637 enable_noise_robust: true,
638 require_multiple_criteria: true,
639 min_criteria_count: 2,
640 enable_plateau_detection: true,
641 noise_window: 10,
642 noise_confidence: 0.99,
643 ..Default::default()
644 },
645 _ => RobustConvergenceOptions {
646 adaptive_tolerance,
647 ..Default::default()
648 },
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn test_robust_convergence_initialization() {
658 let options = RobustConvergenceOptions::default();
659 let mut state = RobustConvergenceState::new(options, 10);
660 state.start_timing();
661
662 assert!(state.start_time.is_some());
663 }
664
665 #[test]
666 fn test_early_stopping() {
667 let mut options = RobustConvergenceOptions::default();
668 options.enable_early_stopping = true;
669 options.early_stopping_patience = 3;
670 options.early_stopping_min_delta = 1e-6;
671
672 let mut state = RobustConvergenceState::new(options, 5);
673
674 for i in 0..5 {
676 let result = state
677 .update_and_check_convergence(1.0, 1e-3, 1e-6, i, None)
678 .unwrap();
679
680 if i >= 3 {
681 assert!(result.converged);
682 assert!(result
683 .convergence_reasons
684 .contains(&"Early stopping".to_string()));
685 }
686 }
687 }
688
689 #[test]
690 fn test_progress_based_convergence() {
691 let mut options = RobustConvergenceOptions::default();
692 options.enable_progress_based = true;
693 options.min_progress_rate = 1e-8;
694
695 let mut state = RobustConvergenceState::new(options, 5);
696
697 for i in 0..15 {
699 let f_val = 1.0 - i as f64 * 1e-10; let result = state
701 .update_and_check_convergence(f_val, 1e-3, 1e-6, i, None)
702 .unwrap();
703
704 if i > 10 {
705 if result.converged {
707 assert!(result
708 .convergence_reasons
709 .contains(&"Insufficient progress".to_string()));
710 break;
711 }
712 }
713 }
714 }
715
716 #[test]
717 fn test_noise_robust_convergence() {
718 let mut options = RobustConvergenceOptions::default();
719 options.enable_noise_robust = true;
720 options.noise_window = 5;
721
722 let mut state = RobustConvergenceState::new(options, 5);
723
724 for i in 0..10 {
726 let noise = if i % 2 == 0 { 1e-9 } else { -1e-9 };
727 let f_val = 1e-10 + noise; let g_val = 1e-6 + noise;
729
730 let result = state
731 .update_and_check_convergence(f_val, g_val, 1e-8, i, None)
732 .unwrap();
733
734 if i >= 8 && result.converged {
735 assert!(result
736 .convergence_reasons
737 .iter()
738 .any(|r| r.contains("Noise-robust") || r.contains("tolerance")));
739 break;
740 }
741 }
742 }
743
744 #[test]
745 fn test_multiple_criteria_requirement() {
746 let mut options = RobustConvergenceOptions::default();
747 options.require_multiple_criteria = true;
748 options.min_criteria_count = 2;
749 options.adaptive_tolerance.initial_ftol = 1e-6;
750 options.adaptive_tolerance.initial_gtol = 1e-4;
751 options.adaptive_tolerance.initial_xtol = 1e-3;
752 options.enable_early_stopping = false;
754 options.enable_progress_based = false;
755 options.enable_plateau_detection = false;
756 options.enable_noise_robust = false;
757
758 let mut state = RobustConvergenceState::new(options, 5);
759
760 let result = state
762 .update_and_check_convergence(1e-8, 1e-1, 1e-2, 10, None)
763 .unwrap();
764
765 assert!(result.criteria_met < 2);
767 }
768
769 #[test]
770 fn test_plateau_detection() {
771 let mut options = RobustConvergenceOptions::default();
772 options.enable_plateau_detection = true;
773 options.plateau_window = 5;
774 options.plateau_tolerance = 1e-10;
775
776 let mut state = RobustConvergenceState::new(options, 5);
777
778 for i in 0..10 {
780 let result = state
781 .update_and_check_convergence(1.0, 1e-3, 1e-6, i, None)
782 .unwrap();
783
784 if i >= 6 {
785 assert!(result.plateau_detected);
786 if result.converged {
787 assert!(result
788 .convergence_reasons
789 .contains(&"Plateau detected".to_string()));
790 }
791 }
792 }
793 }
794
795 #[test]
796 fn test_problem_specific_options() {
797 let easy = create_robust_options_for_problem("well_conditioned", 10, "easy");
798 assert!(!easy.enable_early_stopping);
799 assert!(!easy.require_multiple_criteria);
800
801 let difficult = create_robust_options_for_problem("ill_conditioned", 100, "difficult");
802 assert!(difficult.enable_early_stopping);
803 assert!(difficult.require_multiple_criteria);
804 assert_eq!(difficult.min_criteria_count, 2);
805
806 let very_difficult = create_robust_options_for_problem("noisy", 1000, "very_difficult");
807 assert!(very_difficult.enable_noise_robust);
808 assert_eq!(very_difficult.noise_window, 10);
809 assert_eq!(very_difficult.early_stopping_patience, 50);
810 }
811
812 #[test]
813 fn test_detailed_report() {
814 let options = RobustConvergenceOptions::default();
815 let mut state = RobustConvergenceState::new(options, 5);
816
817 let result = state
818 .update_and_check_convergence(1e-10, 1e-8, 1e-10, 10, None)
819 .unwrap();
820
821 let report = result.get_detailed_report();
822 assert!(report.contains("Convergence Status:"));
823 assert!(report.contains("Function tolerance:"));
824 assert!(report.contains("Gradient tolerance:"));
825 }
826}