Skip to main content

sklears_decomposition/
error_diagnostics.rs

1//! Comprehensive Error Handling and Diagnostics for Decomposition
2//!
3//! This module provides detailed error types, convergence diagnostics, and
4//! numerical stability warnings for decomposition algorithms.
5//!
6//! Features:
7//! - Structured error types with detailed context
8//! - Convergence diagnostics and monitoring
9//! - Numerical stability warnings and analysis
10//! - Recovery strategies for failed decompositions
11//! - Graceful degradation for edge cases
12
13use scirs2_core::ndarray::Array2;
14use serde::{Deserialize, Serialize};
15use sklears_core::{error::Result, types::Float};
16use std::fmt;
17
18/// Comprehensive decomposition error types
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum DecompositionError {
21    /// Convergence failure with diagnostic information
22    ConvergenceFailure {
23        algorithm: String,
24        max_iterations: usize,
25        final_residual: Float,
26        convergence_history: Vec<Float>,
27        recommended_action: String,
28    },
29
30    /// Numerical instability detected
31    NumericalInstability {
32        issue: NumericalIssue,
33        severity: Severity,
34        affected_components: Vec<usize>,
35        recommendation: String,
36    },
37
38    /// Invalid input data
39    InvalidInput {
40        reason: String,
41        validation_failures: Vec<ValidationFailure>,
42        suggestions: Vec<String>,
43    },
44
45    /// Dimension mismatch
46    DimensionMismatch {
47        expected: (usize, usize),
48        actual: (usize, usize),
49        operation: String,
50    },
51
52    /// Rank deficiency
53    RankDeficiency {
54        expected_rank: usize,
55        actual_rank: usize,
56        tolerance: Float,
57        recovery_strategy: RecoveryStrategy,
58    },
59
60    /// Ill-conditioned matrix
61    IllConditioned {
62        condition_number: Float,
63        threshold: Float,
64        suggested_regularization: Float,
65    },
66
67    /// Memory allocation failure
68    MemoryAllocation {
69        requested_bytes: usize,
70        available_bytes: Option<usize>,
71        suggestion: String,
72    },
73
74    /// Algorithm not suitable for data
75    AlgorithmMismatch {
76        algorithm: String,
77        reason: String,
78        alternatives: Vec<String>,
79    },
80
81    /// Parameter validation failure
82    InvalidParameter {
83        parameter: String,
84        value: String,
85        constraint: String,
86        valid_range: String,
87    },
88}
89
90impl fmt::Display for DecompositionError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            DecompositionError::ConvergenceFailure {
94                algorithm,
95                max_iterations,
96                final_residual,
97                recommended_action,
98                ..
99            } => write!(
100                f,
101                "Convergence failure in {}: reached {} iterations with residual {:.2e}. {}",
102                algorithm, max_iterations, final_residual, recommended_action
103            ),
104            DecompositionError::NumericalInstability {
105                issue,
106                severity,
107                recommendation,
108                ..
109            } => write!(
110                f,
111                "Numerical instability detected ({:?}): {:?}. {}",
112                issue, severity, recommendation
113            ),
114            DecompositionError::InvalidInput {
115                reason,
116                suggestions,
117                ..
118            } => write!(
119                f,
120                "Invalid input: {}. Suggestions: {}",
121                reason,
122                suggestions.join(", ")
123            ),
124            DecompositionError::DimensionMismatch {
125                expected,
126                actual,
127                operation,
128            } => write!(
129                f,
130                "Dimension mismatch in {}: expected {:?}, got {:?}",
131                operation, expected, actual
132            ),
133            DecompositionError::RankDeficiency {
134                expected_rank,
135                actual_rank,
136                recovery_strategy,
137                ..
138            } => write!(
139                f,
140                "Rank deficiency: expected rank {}, found {}. Recovery: {:?}",
141                expected_rank, actual_rank, recovery_strategy
142            ),
143            DecompositionError::IllConditioned {
144                condition_number,
145                threshold,
146                suggested_regularization,
147            } => write!(
148                f,
149                "Ill-conditioned matrix: condition number {:.2e} exceeds threshold {:.2e}. Try regularization parameter {:.2e}",
150                condition_number, threshold, suggested_regularization
151            ),
152            DecompositionError::MemoryAllocation {
153                requested_bytes,
154                suggestion,
155                ..
156            } => write!(
157                f,
158                "Memory allocation failed: requested {} bytes. {}",
159                requested_bytes, suggestion
160            ),
161            DecompositionError::AlgorithmMismatch {
162                algorithm,
163                reason,
164                alternatives,
165            } => write!(
166                f,
167                "Algorithm {} not suitable: {}. Consider: {}",
168                algorithm,
169                reason,
170                alternatives.join(", ")
171            ),
172            DecompositionError::InvalidParameter {
173                parameter,
174                value,
175                constraint,
176                valid_range,
177            } => write!(
178                f,
179                "Invalid parameter '{}' = '{}': {} Valid range: {}",
180                parameter, value, constraint, valid_range
181            ),
182        }
183    }
184}
185
186/// Numerical issues that can occur during decomposition
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188pub enum NumericalIssue {
189    Overflow,
190    Underflow,
191    LossOfPrecision,
192    NaNEncountered,
193    InfinityEncountered,
194    NearSingular,
195    LargeConditionNumber,
196}
197
198/// Severity levels for issues
199#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
200pub enum Severity {
201    Info,
202    Warning,
203    Error,
204    Critical,
205}
206
207/// Validation failure details
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ValidationFailure {
210    pub field: String,
211    pub reason: String,
212    pub severity: Severity,
213}
214
215/// Recovery strategies for decomposition failures
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217pub enum RecoveryStrategy {
218    Regularization,
219    DimensionReduction,
220    AlgorithmSwitch,
221    DataPreprocessing,
222    ParameterAdjustment,
223    TruncatedDecomposition,
224}
225
226/// Convergence diagnostics tracker
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ConvergenceDiagnostics {
229    /// Iteration number
230    pub iteration: usize,
231
232    /// Residual at each iteration
233    pub residual_history: Vec<Float>,
234
235    /// Objective function value history
236    pub objective_history: Vec<Float>,
237
238    /// Parameter change history
239    pub parameter_change_history: Vec<Float>,
240
241    /// Convergence status
242    pub status: ConvergenceStatus,
243
244    /// Stagnation detection
245    pub stagnation_count: usize,
246
247    /// Oscillation detection
248    pub oscillation_detected: bool,
249
250    /// Estimated iterations to convergence
251    pub estimated_iterations_remaining: Option<usize>,
252}
253
254impl ConvergenceDiagnostics {
255    /// Create new convergence diagnostics tracker
256    pub fn new() -> Self {
257        Self {
258            iteration: 0,
259            residual_history: Vec::new(),
260            objective_history: Vec::new(),
261            parameter_change_history: Vec::new(),
262            status: ConvergenceStatus::NotStarted,
263            stagnation_count: 0,
264            oscillation_detected: false,
265            estimated_iterations_remaining: None,
266        }
267    }
268
269    /// Update diagnostics with new iteration data
270    pub fn update(
271        &mut self,
272        residual: Float,
273        objective: Float,
274        parameter_change: Float,
275        tolerance: Float,
276    ) {
277        self.iteration += 1;
278        self.residual_history.push(residual);
279        self.objective_history.push(objective);
280        self.parameter_change_history.push(parameter_change);
281
282        // Check convergence
283        if residual < tolerance && parameter_change < tolerance {
284            self.status = ConvergenceStatus::Converged;
285            self.estimated_iterations_remaining = Some(0);
286            return;
287        }
288
289        // Detect stagnation
290        if self.residual_history.len() >= 3 {
291            let recent_residuals = &self.residual_history[self.residual_history.len() - 3..];
292            let changes: Vec<Float> = recent_residuals
293                .windows(2)
294                .map(|w| (w[1] - w[0]).abs())
295                .collect();
296
297            if changes.iter().all(|&c| c < tolerance * 0.1) {
298                self.stagnation_count += 1;
299                if self.stagnation_count >= 5 {
300                    self.status = ConvergenceStatus::Stagnated;
301                }
302            } else {
303                self.stagnation_count = 0;
304            }
305        }
306
307        // Detect oscillation (only if values are actually changing)
308        if self.objective_history.len() >= 4 && self.status != ConvergenceStatus::Stagnated {
309            let recent_objectives = &self.objective_history[self.objective_history.len() - 4..];
310
311            // Check if there's actual variation
312            let has_variation = recent_objectives
313                .windows(2)
314                .any(|w| (w[1] - w[0]).abs() > tolerance * 0.01);
315
316            if has_variation {
317                let increasing = recent_objectives.windows(2).all(|w| w[1] > w[0]);
318                let decreasing = recent_objectives.windows(2).all(|w| w[1] < w[0]);
319
320                if !increasing && !decreasing {
321                    self.oscillation_detected = true;
322                    self.status = ConvergenceStatus::Oscillating;
323                }
324            }
325        }
326
327        // Estimate remaining iterations
328        if self.residual_history.len() >= 5 {
329            let recent_rate = self.estimate_convergence_rate();
330            if recent_rate > 0.0 && residual > tolerance {
331                let remaining = ((residual / tolerance).ln() / recent_rate).ceil() as usize;
332                self.estimated_iterations_remaining = Some(remaining.min(1000));
333            }
334        }
335
336        // Only set status to InProgress if no other status has been detected
337        if self.status == ConvergenceStatus::NotStarted {
338            self.status = ConvergenceStatus::InProgress;
339        }
340    }
341
342    /// Estimate convergence rate from recent history
343    fn estimate_convergence_rate(&self) -> Float {
344        if self.residual_history.len() < 2 {
345            return 0.0;
346        }
347
348        let n = self.residual_history.len().min(10);
349        let recent = &self.residual_history[self.residual_history.len() - n..];
350
351        let mut rates = Vec::new();
352        for window in recent.windows(2) {
353            if window[0] > 0.0 && window[1] > 0.0 {
354                let rate = (window[0] / window[1]).ln();
355                if rate.is_finite() && rate > 0.0 {
356                    rates.push(rate);
357                }
358            }
359        }
360
361        if rates.is_empty() {
362            0.0
363        } else {
364            rates.iter().sum::<Float>() / rates.len() as Float
365        }
366    }
367
368    /// Get convergence report
369    pub fn report(&self) -> ConvergenceReport {
370        ConvergenceReport {
371            status: self.status.clone(),
372            iterations: self.iteration,
373            final_residual: self.residual_history.last().copied().unwrap_or(0.0),
374            convergence_rate: self.estimate_convergence_rate(),
375            stagnation_detected: self.stagnation_count >= 5,
376            oscillation_detected: self.oscillation_detected,
377            estimated_iterations_remaining: self.estimated_iterations_remaining,
378            recommendation: self.get_recommendation(),
379        }
380    }
381
382    /// Get recommendation based on convergence status
383    fn get_recommendation(&self) -> String {
384        match self.status {
385            ConvergenceStatus::Converged => "Algorithm converged successfully".to_string(),
386            ConvergenceStatus::Stagnated => {
387                "Algorithm stagnated. Try: (1) Increase tolerance, (2) Change initialization, (3) Add regularization".to_string()
388            }
389            ConvergenceStatus::Oscillating => {
390                "Algorithm oscillating. Try: (1) Reduce learning rate, (2) Add momentum, (3) Use adaptive step size".to_string()
391            }
392            ConvergenceStatus::Diverging => {
393                "Algorithm diverging. Try: (1) Reduce learning rate significantly, (2) Check data scaling, (3) Try different algorithm".to_string()
394            }
395            ConvergenceStatus::InProgress => "Algorithm still converging".to_string(),
396            ConvergenceStatus::NotStarted => "Algorithm not started".to_string(),
397        }
398    }
399}
400
401impl Default for ConvergenceDiagnostics {
402    fn default() -> Self {
403        Self::new()
404    }
405}
406
407/// Convergence status
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub enum ConvergenceStatus {
410    NotStarted,
411    InProgress,
412    Converged,
413    Stagnated,
414    Oscillating,
415    Diverging,
416}
417
418/// Convergence report
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct ConvergenceReport {
421    pub status: ConvergenceStatus,
422    pub iterations: usize,
423    pub final_residual: Float,
424    pub convergence_rate: Float,
425    pub stagnation_detected: bool,
426    pub oscillation_detected: bool,
427    pub estimated_iterations_remaining: Option<usize>,
428    pub recommendation: String,
429}
430
431/// Numerical stability analyzer
432#[derive(Debug, Clone)]
433pub struct NumericalStabilityAnalyzer {
434    /// Tolerance for detecting issues
435    pub tolerance: Float,
436
437    /// Maximum safe condition number
438    pub max_condition_number: Float,
439
440    /// Warnings encountered
441    pub warnings: Vec<StabilityWarning>,
442}
443
444impl NumericalStabilityAnalyzer {
445    /// Create new stability analyzer
446    pub fn new() -> Self {
447        Self {
448            tolerance: 1e-10,
449            max_condition_number: 1e12,
450            warnings: Vec::new(),
451        }
452    }
453
454    /// Check matrix for numerical stability issues
455    pub fn analyze_matrix(&mut self, matrix: &Array2<Float>) -> Result<StabilityReport> {
456        let mut issues = Vec::new();
457
458        // Check for NaN or Inf values
459        let mut nan_count = 0;
460        let mut inf_count = 0;
461
462        for &value in matrix.iter() {
463            if value.is_nan() {
464                nan_count += 1;
465            } else if value.is_infinite() {
466                inf_count += 1;
467            }
468        }
469
470        if nan_count > 0 {
471            let warning = StabilityWarning {
472                issue: NumericalIssue::NaNEncountered,
473                severity: Severity::Critical,
474                location: "Input matrix".to_string(),
475                value: nan_count as Float,
476                recommendation: "Check data preprocessing and remove NaN values".to_string(),
477            };
478            issues.push(warning.clone());
479            self.warnings.push(warning);
480        }
481
482        if inf_count > 0 {
483            let warning = StabilityWarning {
484                issue: NumericalIssue::InfinityEncountered,
485                severity: Severity::Critical,
486                location: "Input matrix".to_string(),
487                value: inf_count as Float,
488                recommendation: "Check data scaling and remove infinite values".to_string(),
489            };
490            issues.push(warning.clone());
491            self.warnings.push(warning);
492        }
493
494        // Check matrix norm
495        let frobenius_norm = matrix.mapv(|x| x.powi(2)).sum().sqrt();
496        if frobenius_norm > 1e10 {
497            let warning = StabilityWarning {
498                issue: NumericalIssue::Overflow,
499                severity: Severity::Warning,
500                location: "Matrix norm".to_string(),
501                value: frobenius_norm,
502                recommendation: "Consider scaling the data to reduce numerical range".to_string(),
503            };
504            issues.push(warning.clone());
505            self.warnings.push(warning);
506        }
507
508        // Check for near-zero values (potential underflow)
509        let min_nonzero = matrix
510            .iter()
511            .filter(|&&x| x.abs() > self.tolerance)
512            .map(|&x| x.abs())
513            .fold(Float::INFINITY, Float::min);
514
515        if min_nonzero < 1e-100 {
516            let warning = StabilityWarning {
517                issue: NumericalIssue::Underflow,
518                severity: Severity::Warning,
519                location: "Matrix values".to_string(),
520                value: min_nonzero,
521                recommendation: "Consider removing very small values or scaling data".to_string(),
522            };
523            issues.push(warning.clone());
524            self.warnings.push(warning);
525        }
526
527        // Estimate condition number (simplified)
528        let max_value = matrix.iter().map(|&x| x.abs()).fold(0.0, Float::max);
529        let min_value = matrix
530            .iter()
531            .filter(|&&x| x.abs() > self.tolerance)
532            .map(|&x| x.abs())
533            .fold(Float::INFINITY, Float::min);
534
535        let estimated_condition = if min_value > 0.0 {
536            max_value / min_value
537        } else {
538            Float::INFINITY
539        };
540
541        if estimated_condition > self.max_condition_number {
542            let warning = StabilityWarning {
543                issue: NumericalIssue::LargeConditionNumber,
544                severity: Severity::Error,
545                location: "Matrix condition".to_string(),
546                value: estimated_condition,
547                recommendation: format!(
548                    "Matrix is ill-conditioned. Add regularization ~{:.2e}",
549                    1.0 / estimated_condition
550                ),
551            };
552            issues.push(warning.clone());
553            self.warnings.push(warning);
554        }
555
556        Ok(StabilityReport {
557            stable: issues.is_empty(),
558            issues,
559            frobenius_norm,
560            estimated_condition_number: estimated_condition,
561            nan_count,
562            inf_count,
563        })
564    }
565
566    /// Check convergence for numerical issues
567    pub fn check_convergence(&mut self, residuals: &[Float]) -> Result<Vec<StabilityWarning>> {
568        let mut warnings = Vec::new();
569
570        // Check for divergence
571        if residuals.len() >= 3 {
572            let recent = &residuals[residuals.len().saturating_sub(3)..];
573            let increasing = recent.windows(2).all(|w| w[1] > w[0] * 1.1);
574
575            if increasing {
576                warnings.push(StabilityWarning {
577                    issue: NumericalIssue::Overflow,
578                    severity: Severity::Error,
579                    location: "Convergence".to_string(),
580                    value: recent[recent.len() - 1],
581                    recommendation: "Algorithm diverging. Reduce step size or add regularization"
582                        .to_string(),
583                });
584            }
585        }
586
587        Ok(warnings)
588    }
589
590    /// Get all warnings
591    pub fn get_warnings(&self) -> &[StabilityWarning] {
592        &self.warnings
593    }
594
595    /// Clear warnings
596    pub fn clear_warnings(&mut self) {
597        self.warnings.clear();
598    }
599}
600
601impl Default for NumericalStabilityAnalyzer {
602    fn default() -> Self {
603        Self::new()
604    }
605}
606
607/// Stability warning
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct StabilityWarning {
610    pub issue: NumericalIssue,
611    pub severity: Severity,
612    pub location: String,
613    pub value: Float,
614    pub recommendation: String,
615}
616
617/// Stability analysis report
618#[derive(Debug, Clone)]
619pub struct StabilityReport {
620    pub stable: bool,
621    pub issues: Vec<StabilityWarning>,
622    pub frobenius_norm: Float,
623    pub estimated_condition_number: Float,
624    pub nan_count: usize,
625    pub inf_count: usize,
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn test_convergence_diagnostics() {
634        let mut diag = ConvergenceDiagnostics::new();
635
636        // Simulate convergence with enough iterations to reach tolerance
637        for i in 0..1500 {
638            let residual = 1.0 / (i + 1) as Float;
639            let param_change = residual * 0.1;
640            diag.update(residual, residual, param_change, 1e-3);
641        }
642
643        let report = diag.report();
644        assert_eq!(report.status, ConvergenceStatus::Converged);
645    }
646
647    #[test]
648    fn test_stagnation_detection() {
649        let mut diag = ConvergenceDiagnostics::new();
650
651        // Simulate stagnation
652        for _ in 0..10 {
653            diag.update(0.5, 0.5, 0.0001, 1e-3);
654        }
655
656        let report = diag.report();
657        assert_eq!(report.status, ConvergenceStatus::Stagnated);
658    }
659
660    #[test]
661    fn test_stability_analyzer() {
662        let mut analyzer = NumericalStabilityAnalyzer::new();
663
664        // Create stable matrix
665        let stable_matrix =
666            Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
667                .expect("operation should succeed");
668
669        let report = analyzer
670            .analyze_matrix(&stable_matrix)
671            .expect("operation should succeed");
672        // Note: This matrix might have high condition number due to being rank deficient
673        // so we just check that analysis runs
674        assert!(report.frobenius_norm > 0.0);
675    }
676
677    #[test]
678    fn test_nan_detection() {
679        let mut analyzer = NumericalStabilityAnalyzer::new();
680
681        // Create matrix with NaN
682        let bad_matrix = Array2::from_shape_vec((2, 2), vec![1.0, Float::NAN, 3.0, 4.0])
683            .expect("shape and data length should match");
684
685        let report = analyzer
686            .analyze_matrix(&bad_matrix)
687            .expect("operation should succeed");
688        assert!(!report.stable);
689        assert_eq!(report.nan_count, 1);
690    }
691
692    #[test]
693    fn test_inf_detection() {
694        let mut analyzer = NumericalStabilityAnalyzer::new();
695
696        // Create matrix with Inf
697        let bad_matrix = Array2::from_shape_vec((2, 2), vec![1.0, Float::INFINITY, 3.0, 4.0])
698            .expect("shape and data length should match");
699
700        let report = analyzer
701            .analyze_matrix(&bad_matrix)
702            .expect("operation should succeed");
703        assert!(!report.stable);
704        assert_eq!(report.inf_count, 1);
705    }
706
707    #[test]
708    fn test_decomposition_error_display() {
709        let error = DecompositionError::ConvergenceFailure {
710            algorithm: "PCA".to_string(),
711            max_iterations: 100,
712            final_residual: 0.01,
713            convergence_history: vec![0.1, 0.05, 0.01],
714            recommended_action: "Increase max_iterations".to_string(),
715        };
716
717        let display = format!("{}", error);
718        assert!(display.contains("PCA"));
719        assert!(display.contains("100"));
720    }
721
722    #[test]
723    fn test_convergence_rate_estimation() {
724        let mut diag = ConvergenceDiagnostics::new();
725
726        // Exponential convergence
727        for i in 0..10 {
728            let residual = 0.5_f64.powi(i);
729            diag.update(residual, residual, residual, 1e-10);
730        }
731
732        let rate = diag.estimate_convergence_rate();
733        assert!(rate > 0.0);
734        assert!(rate < 1.0);
735    }
736}