1use scirs2_core::ndarray::Array2;
14use serde::{Deserialize, Serialize};
15use sklears_core::{error::Result, types::Float};
16use std::fmt;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum DecompositionError {
21 ConvergenceFailure {
23 algorithm: String,
24 max_iterations: usize,
25 final_residual: Float,
26 convergence_history: Vec<Float>,
27 recommended_action: String,
28 },
29
30 NumericalInstability {
32 issue: NumericalIssue,
33 severity: Severity,
34 affected_components: Vec<usize>,
35 recommendation: String,
36 },
37
38 InvalidInput {
40 reason: String,
41 validation_failures: Vec<ValidationFailure>,
42 suggestions: Vec<String>,
43 },
44
45 DimensionMismatch {
47 expected: (usize, usize),
48 actual: (usize, usize),
49 operation: String,
50 },
51
52 RankDeficiency {
54 expected_rank: usize,
55 actual_rank: usize,
56 tolerance: Float,
57 recovery_strategy: RecoveryStrategy,
58 },
59
60 IllConditioned {
62 condition_number: Float,
63 threshold: Float,
64 suggested_regularization: Float,
65 },
66
67 MemoryAllocation {
69 requested_bytes: usize,
70 available_bytes: Option<usize>,
71 suggestion: String,
72 },
73
74 AlgorithmMismatch {
76 algorithm: String,
77 reason: String,
78 alternatives: Vec<String>,
79 },
80
81 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
200pub enum Severity {
201 Info,
202 Warning,
203 Error,
204 Critical,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct ValidationFailure {
210 pub field: String,
211 pub reason: String,
212 pub severity: Severity,
213}
214
215#[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#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ConvergenceDiagnostics {
229 pub iteration: usize,
231
232 pub residual_history: Vec<Float>,
234
235 pub objective_history: Vec<Float>,
237
238 pub parameter_change_history: Vec<Float>,
240
241 pub status: ConvergenceStatus,
243
244 pub stagnation_count: usize,
246
247 pub oscillation_detected: bool,
249
250 pub estimated_iterations_remaining: Option<usize>,
252}
253
254impl ConvergenceDiagnostics {
255 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 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 if residual < tolerance && parameter_change < tolerance {
284 self.status = ConvergenceStatus::Converged;
285 self.estimated_iterations_remaining = Some(0);
286 return;
287 }
288
289 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 if self.objective_history.len() >= 4 && self.status != ConvergenceStatus::Stagnated {
309 let recent_objectives = &self.objective_history[self.objective_history.len() - 4..];
310
311 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 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 if self.status == ConvergenceStatus::NotStarted {
338 self.status = ConvergenceStatus::InProgress;
339 }
340 }
341
342 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 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 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#[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#[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#[derive(Debug, Clone)]
433pub struct NumericalStabilityAnalyzer {
434 pub tolerance: Float,
436
437 pub max_condition_number: Float,
439
440 pub warnings: Vec<StabilityWarning>,
442}
443
444impl NumericalStabilityAnalyzer {
445 pub fn new() -> Self {
447 Self {
448 tolerance: 1e-10,
449 max_condition_number: 1e12,
450 warnings: Vec::new(),
451 }
452 }
453
454 pub fn analyze_matrix(&mut self, matrix: &Array2<Float>) -> Result<StabilityReport> {
456 let mut issues = Vec::new();
457
458 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 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 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 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 pub fn check_convergence(&mut self, residuals: &[Float]) -> Result<Vec<StabilityWarning>> {
568 let mut warnings = Vec::new();
569
570 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 pub fn get_warnings(&self) -> &[StabilityWarning] {
592 &self.warnings
593 }
594
595 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#[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#[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 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 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 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 assert!(report.frobenius_norm > 0.0);
675 }
676
677 #[test]
678 fn test_nan_detection() {
679 let mut analyzer = NumericalStabilityAnalyzer::new();
680
681 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 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 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}