scirs2_optimize/unconstrained/
robust_convergence.rs

1//! Robust convergence criteria with multiple stopping conditions
2//!
3//! This module provides robust convergence detection that handles:
4//! - Multiple simultaneous stopping criteria
5//! - Early stopping for noisy or difficult functions
6//! - Numerical stability considerations
7//! - Progress-based convergence detection
8//! - Time-based stopping conditions
9
10use 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/// Robust convergence options with multiple stopping criteria
19#[derive(Debug, Clone)]
20pub struct RobustConvergenceOptions {
21    /// Adaptive tolerance options
22    pub adaptive_tolerance: AdaptiveToleranceOptions,
23    /// Enable early stopping for difficult problems
24    pub enable_early_stopping: bool,
25    /// Early stopping patience (iterations without improvement)
26    pub early_stopping_patience: usize,
27    /// Relative improvement threshold for early stopping
28    pub early_stopping_min_delta: f64,
29    /// Enable progress-based convergence
30    pub enable_progress_based: bool,
31    /// Number of iterations to consider for progress evaluation
32    pub progress_window: usize,
33    /// Minimum progress rate to continue optimization
34    pub min_progress_rate: f64,
35    /// Enable time-based stopping
36    pub enable_time_limit: bool,
37    /// Maximum optimization time
38    pub max_time: Duration,
39    /// Enable robust noisy function handling
40    pub enable_noise_robust: bool,
41    /// Window size for noise-robust convergence
42    pub noise_window: usize,
43    /// Confidence level for noise-robust convergence (0.0 to 1.0)
44    pub noise_confidence: f64,
45    /// Enable multiple criteria requirement
46    pub require_multiple_criteria: bool,
47    /// Number of criteria that must be satisfied simultaneously
48    pub min_criteria_count: usize,
49    /// Enable plateau detection
50    pub enable_plateau_detection: bool,
51    /// Plateau tolerance
52    pub plateau_tolerance: f64,
53    /// Plateau window size
54    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), // 1 hour default
69            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/// State for robust convergence detection
82#[derive(Debug)]
83pub struct RobustConvergenceState {
84    /// Adaptive tolerance state
85    adaptive_state: AdaptiveToleranceState,
86    /// Early stopping state
87    early_stop_state: EarlyStoppingState,
88    /// Progress tracking state
89    progress_state: ProgressState,
90    /// Time tracking
91    start_time: Option<Instant>,
92    /// Noise robust state
93    noise_state: NoiseRobustState,
94    /// Plateau detection state
95    plateau_state: PlateauState,
96    /// Options
97    options: RobustConvergenceOptions,
98}
99
100/// Early stopping state
101#[derive(Debug, Clone)]
102struct EarlyStoppingState {
103    best_value: f64,
104    iterations_without_improvement: usize,
105    improvement_history: VecDeque<f64>,
106}
107
108/// Progress tracking state
109#[derive(Debug, Clone)]
110struct ProgressState {
111    recent_values: VecDeque<f64>,
112    recent_gradients: VecDeque<f64>,
113    progress_rate: f64,
114}
115
116/// Noise-robust convergence state
117#[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/// Plateau detection state
127#[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    /// Create new robust convergence state
136    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    /// Start timing for time-based convergence
169    pub fn start_timing(&mut self) {
170        if self.options.enable_time_limit {
171            self.start_time = Some(Instant::now());
172        }
173    }
174
175    /// Update state and check all convergence criteria
176    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        // Update adaptive tolerance state
185        self.adaptive_state
186            .update(function_value, gradient_norm, step_norm, iteration)?;
187
188        // Update early stopping
189        if self.options.enable_early_stopping {
190            self.update_early_stopping(function_value, iteration);
191        }
192
193        // Update progress tracking
194        if self.options.enable_progress_based {
195            self.update_progress(function_value, gradient_norm);
196        }
197
198        // Update noise-robust state
199        if self.options.enable_noise_robust {
200            self.update_noise_robust(function_value, gradient_norm, step_norm);
201        }
202
203        // Update plateau detection
204        if self.options.enable_plateau_detection {
205            self.update_plateau_detection(function_value, iteration);
206        }
207
208        // Check all convergence criteria
209        self.check_all_convergence_criteria(function_value, gradient_norm, step_norm, iteration)
210    }
211
212    /// Update early stopping state
213    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        // Track improvement history
224        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    /// Update progress tracking
233    fn update_progress(&mut self, function_value: f64, gradient_norm: f64) {
234        // Add to recent values
235        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        // Calculate progress rate
248        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            // Linear regression to estimate progress rate
253            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    /// Update noise-robust convergence state
270    fn update_noise_robust(&mut self, function_value: f64, gradient_norm: f64, step_norm: f64) {
271        // Add to windows
272        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    /// Update plateau detection
289    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        // Check for plateau
296        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    /// Check all convergence criteria
314    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        // Get function change
325        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        // Check adaptive convergence
333        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        // Standard convergence criteria
340        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        // Early stopping check
354        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        // Progress-based convergence
365        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        // Time limit check
373        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        // Noise-robust convergence
383        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        // Plateau detection
392        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        // Check if convergence is achieved
398        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        // Generate warnings
405        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    /// Check noise-robust convergence
434    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        // Statistical test for convergence stability
440        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        // Check if recent values are consistently small (statistically)
444        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        // Use confidence interval approach
455        let f_margin = f_std * 1.96; // 95% confidence
456        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    /// Calculate noise confidence level
465    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        // Confidence based on coefficient of variation
475        let cv = if mean.abs() > 1e-15 {
476            variance.sqrt() / mean.abs()
477        } else {
478            f64::INFINITY
479        };
480
481        // Map CV to confidence (0 = perfect, high CV = low confidence)
482        (1.0 / (1.0 + cv)).clamp(0.0, 1.0)
483    }
484
485    /// Get current adaptive state for compatibility
486    pub fn get_adaptive_state(&self) -> &AdaptiveToleranceState {
487        &self.adaptive_state
488    }
489}
490
491/// Comprehensive convergence result with detailed information
492#[derive(Debug, Clone)]
493pub struct RobustConvergenceResult {
494    /// Whether convergence was achieved
495    pub converged: bool,
496    /// List of reasons for convergence
497    pub convergence_reasons: Vec<String>,
498    /// Warning flags for potential issues
499    pub warning_flags: Vec<String>,
500    /// Adaptive convergence status
501    pub adaptive_status: ConvergenceStatus,
502    /// Number of criteria that were met
503    pub criteria_met: usize,
504    /// Early stopping iterations count
505    pub early_stopping_nit: usize,
506    /// Current progress rate
507    pub progress_rate: f64,
508    /// Time elapsed since start
509    pub time_elapsed: Option<Duration>,
510    /// Whether plateau was detected
511    pub plateau_detected: bool,
512    /// Confidence level for noise-robust convergence
513    pub noise_robust_confidence: f64,
514}
515
516impl RobustConvergenceResult {
517    /// Get convergence message
518    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    /// Get detailed status report
534    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/// Create robust convergence options for specific problem types
592#[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        // Simulate no improvement for several iterations
675        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        // Simulate very slow progress
698        for i in 0..15 {
699            let f_val = 1.0 - i as f64 * 1e-10; // Extremely slow decrease
700            let result = state
701                .update_and_check_convergence(f_val, 1e-3, 1e-6, i, None)
702                .unwrap();
703
704            if i > 10 {
705                // Should converge due to insufficient progress
706                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        // Simulate noisy convergence
725        for i in 0..10 {
726            let noise = if i % 2 == 0 { 1e-9 } else { -1e-9 };
727            let f_val = 1e-10 + noise; // Converged but noisy
728            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        // Disable other convergence criteria to test only basic tolerances
753        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        // Only satisfy one criterion (function tolerance)
761        let result = state
762            .update_and_check_convergence(1e-8, 1e-1, 1e-2, 10, None)
763            .unwrap();
764
765        // Should not converge with only one criterion met
766        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        // Simulate plateau
779        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}