Skip to main content

sublinear_solver/
error.rs

1//! Error types and handling for the sublinear solver.
2//!
3//! This module defines all error conditions that can occur during matrix operations
4//! and solver execution, providing detailed error information for debugging and
5//! recovery strategies.
6
7use alloc::{string::String, vec::Vec};
8use core::fmt;
9
10/// Result type alias for solver operations.
11pub type Result<T> = core::result::Result<T, SolverError>;
12
13/// Comprehensive error type for all solver operations.
14#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum SolverError {
17    /// Matrix is not diagonally dominant, which is required for convergence guarantees.
18    MatrixNotDiagonallyDominant {
19        /// The row where diagonal dominance fails
20        row: usize,
21        /// Diagonal element value
22        diagonal: f64,
23        /// Sum of off-diagonal absolute values
24        off_diagonal_sum: f64,
25    },
26
27    /// Numerical instability detected during computation.
28    NumericalInstability {
29        /// Description of the instability
30        reason: String,
31        /// Iteration where instability was detected
32        iteration: usize,
33        /// Current residual norm when instability occurred
34        residual_norm: f64,
35    },
36
37    /// Coherence gate refused the solve: the matrix's diagonal-dominance
38    /// margin dropped below the configured threshold, so the solver would
39    /// have spent polynomial-time work on a near-singular system to
40    /// produce an ε-quality answer. See ADR-001 (Complexity as Architecture)
41    /// roadmap item #3 and `src/coherence.rs` for the gate implementation.
42    Incoherent {
43        /// Computed coherence score in [-∞, 1]: 1.0 = perfectly diagonal,
44        /// > 0 = strictly diagonally dominant, ≤ 0 = at or past the
45        /// diagonal-dominance boundary.
46        coherence: f64,
47        /// Threshold the caller configured via
48        /// `SolverOptions::coherence_threshold`.
49        threshold: f64,
50    },
51
52    /// Algorithm failed to converge within specified iterations.
53    ConvergenceFailure {
54        /// Number of iterations performed
55        iterations: usize,
56        /// Final residual norm achieved
57        residual_norm: f64,
58        /// Target tolerance that wasn't reached
59        tolerance: f64,
60        /// Algorithm that failed to converge
61        algorithm: String,
62    },
63
64    /// Invalid input parameters or data.
65    InvalidInput {
66        /// Description of the invalid input
67        message: String,
68        /// Optional parameter name that was invalid
69        parameter: Option<String>,
70    },
71
72    /// Dimension mismatch between matrix and vector operations.
73    DimensionMismatch {
74        /// Expected dimension
75        expected: usize,
76        /// Actual dimension found
77        actual: usize,
78        /// Context where mismatch occurred
79        operation: String,
80    },
81
82    /// Matrix format is not supported for the requested operation.
83    UnsupportedMatrixFormat {
84        /// Current matrix format
85        current_format: String,
86        /// Required format for the operation
87        required_format: String,
88        /// Operation that was attempted
89        operation: String,
90    },
91
92    /// Memory allocation failure.
93    MemoryAllocationError {
94        /// Requested allocation size in bytes
95        requested_size: usize,
96        /// Available memory at time of failure (if known)
97        available_memory: Option<usize>,
98    },
99
100    /// Index out of bounds for matrix or vector access.
101    IndexOutOfBounds {
102        /// The invalid index
103        index: usize,
104        /// Maximum valid index
105        max_index: usize,
106        /// Context where out-of-bounds access occurred
107        context: String,
108    },
109
110    /// Sparse matrix contains invalid data.
111    InvalidSparseMatrix {
112        /// Description of the invalid data
113        reason: String,
114        /// Position where invalid data was found
115        position: Option<(usize, usize)>,
116    },
117
118    /// Algorithm-specific error conditions.
119    AlgorithmError {
120        /// Name of the algorithm
121        algorithm: String,
122        /// Specific error message
123        message: String,
124        /// Additional context data
125        context: Vec<(String, String)>,
126    },
127
128    /// WebAssembly binding error (when WASM feature is enabled).
129    #[cfg(feature = "wasm")]
130    WasmBindingError {
131        /// Error message from WASM binding
132        message: String,
133        /// JavaScript error if available
134        js_error: Option<String>,
135    },
136
137    /// I/O error for file operations (when std feature is enabled).
138    #[cfg(feature = "std")]
139    IoError {
140        /// I/O error description
141        #[cfg_attr(feature = "serde", serde(skip))]
142        message: String,
143        /// Context where I/O error occurred
144        context: String,
145    },
146
147    /// Serialization/deserialization error.
148    #[cfg(feature = "serde")]
149    SerializationError {
150        /// Error message from serialization
151        message: String,
152        /// Data type being serialized
153        data_type: String,
154    },
155}
156
157impl SolverError {
158    /// Check if this error indicates a recoverable condition.
159    ///
160    /// Recoverable errors can potentially be resolved by adjusting
161    /// algorithm parameters or switching to a different solver.
162    pub fn is_recoverable(&self) -> bool {
163        match self {
164            SolverError::ConvergenceFailure { .. } => true,
165            SolverError::NumericalInstability { .. } => true,
166            // Incoherent is recoverable in the same sense ConvergenceFailure
167            // is: the caller can lower the coherence_threshold, fall back to
168            // a cached answer, or refuse the solve. The matrix itself is not
169            // broken — it's just below the budget the caller asked for.
170            SolverError::Incoherent { .. } => true,
171            SolverError::MatrixNotDiagonallyDominant { .. } => false, // Fundamental issue
172            SolverError::InvalidInput { .. } => false,                // User error
173            SolverError::DimensionMismatch { .. } => false,           // User error
174            SolverError::MemoryAllocationError { .. } => false,       // System limitation
175            SolverError::IndexOutOfBounds { .. } => false,            // Programming error
176            SolverError::InvalidSparseMatrix { .. } => false,         // Data corruption
177            SolverError::UnsupportedMatrixFormat { .. } => true,      // Can convert format
178            SolverError::AlgorithmError { .. } => true, // Algorithm-specific, might recover
179            #[cfg(feature = "wasm")]
180            SolverError::WasmBindingError { .. } => false, // Runtime environment issue
181            #[cfg(feature = "std")]
182            SolverError::IoError { .. } => false, // External system issue
183            #[cfg(feature = "serde")]
184            SolverError::SerializationError { .. } => false, // Data format issue
185        }
186    }
187
188    /// Get suggested recovery strategy for recoverable errors.
189    pub fn recovery_strategy(&self) -> Option<RecoveryStrategy> {
190        match self {
191            SolverError::ConvergenceFailure { algorithm, .. } => {
192                // Suggest alternative algorithms
193                Some(match algorithm.as_str() {
194                    "neumann" => RecoveryStrategy::SwitchAlgorithm("hybrid".to_string()),
195                    "forward_push" => {
196                        RecoveryStrategy::SwitchAlgorithm("backward_push".to_string())
197                    }
198                    "backward_push" => RecoveryStrategy::SwitchAlgorithm("hybrid".to_string()),
199                    _ => RecoveryStrategy::RelaxTolerance(10.0),
200                })
201            }
202            SolverError::NumericalInstability { .. } => Some(RecoveryStrategy::IncreasePrecision),
203            SolverError::UnsupportedMatrixFormat {
204                required_format, ..
205            } => Some(RecoveryStrategy::ConvertMatrixFormat(
206                required_format.clone(),
207            )),
208            SolverError::AlgorithmError { algorithm, .. } => {
209                Some(RecoveryStrategy::SwitchAlgorithm("neumann".to_string()))
210            }
211            _ => None,
212        }
213    }
214
215    /// Get the error severity level.
216    pub fn severity(&self) -> ErrorSeverity {
217        match self {
218            SolverError::MemoryAllocationError { .. } => ErrorSeverity::Critical,
219            SolverError::InvalidSparseMatrix { .. } => ErrorSeverity::Critical,
220            SolverError::IndexOutOfBounds { .. } => ErrorSeverity::Critical,
221            SolverError::MatrixNotDiagonallyDominant { .. } => ErrorSeverity::High,
222            SolverError::ConvergenceFailure { .. } => ErrorSeverity::Medium,
223            SolverError::NumericalInstability { .. } => ErrorSeverity::Medium,
224            // Incoherent is a *budget* refusal, not a data corruption — Low.
225            // The caller asked us to refuse, we refused; nothing's broken.
226            SolverError::Incoherent { .. } => ErrorSeverity::Low,
227            SolverError::InvalidInput { .. } => ErrorSeverity::Medium,
228            SolverError::DimensionMismatch { .. } => ErrorSeverity::Medium,
229            SolverError::UnsupportedMatrixFormat { .. } => ErrorSeverity::Low,
230            SolverError::AlgorithmError { .. } => ErrorSeverity::Medium,
231            #[cfg(feature = "wasm")]
232            SolverError::WasmBindingError { .. } => ErrorSeverity::High,
233            #[cfg(feature = "std")]
234            SolverError::IoError { .. } => ErrorSeverity::Medium,
235            #[cfg(feature = "serde")]
236            SolverError::SerializationError { .. } => ErrorSeverity::Low,
237        }
238    }
239}
240
241/// Recovery strategies for recoverable errors.
242#[derive(Debug, Clone, PartialEq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244pub enum RecoveryStrategy {
245    /// Switch to a different solver algorithm.
246    SwitchAlgorithm(String),
247    /// Increase numerical precision (f32 -> f64).
248    IncreasePrecision,
249    /// Relax convergence tolerance by the given factor.
250    RelaxTolerance(f64),
251    /// Restart with different random seed.
252    RestartWithDifferentSeed,
253    /// Convert matrix to a different storage format.
254    ConvertMatrixFormat(String),
255    /// Increase maximum iteration count.
256    IncreaseIterations(usize),
257}
258
259/// Error severity levels for logging and monitoring.
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
262pub enum ErrorSeverity {
263    /// Low severity - algorithm can continue with degraded performance
264    Low,
265    /// Medium severity - operation failed but system remains stable
266    Medium,
267    /// High severity - significant failure requiring user intervention
268    High,
269    /// Critical severity - system integrity compromised
270    Critical,
271}
272
273impl fmt::Display for SolverError {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self {
276            SolverError::MatrixNotDiagonallyDominant {
277                row,
278                diagonal,
279                off_diagonal_sum,
280            } => {
281                write!(f, "Matrix is not diagonally dominant at row {}: diagonal = {:.6}, off-diagonal sum = {:.6}", 
282                       row, diagonal, off_diagonal_sum)
283            }
284            SolverError::NumericalInstability {
285                reason,
286                iteration,
287                residual_norm,
288            } => {
289                write!(
290                    f,
291                    "Numerical instability at iteration {}: {} (residual = {:.2e})",
292                    iteration, reason, residual_norm
293                )
294            }
295            SolverError::ConvergenceFailure {
296                iterations,
297                residual_norm,
298                tolerance,
299                algorithm,
300            } => {
301                write!(f, "Algorithm '{}' failed to converge after {} iterations: residual = {:.2e} > tolerance = {:.2e}",
302                       algorithm, iterations, residual_norm, tolerance)
303            }
304            SolverError::Incoherent {
305                coherence,
306                threshold,
307            } => {
308                write!(
309                    f,
310                    "Coherence gate refused solve: matrix coherence = {:.6} < threshold = {:.6} \
311                     (ADR-001 item #3 — set SolverOptions::coherence_threshold to 0.0 to disable)",
312                    coherence, threshold,
313                )
314            }
315            SolverError::InvalidInput { message, parameter } => match parameter {
316                Some(param) => write!(f, "Invalid input for parameter '{}': {}", param, message),
317                None => write!(f, "Invalid input: {}", message),
318            },
319            SolverError::DimensionMismatch {
320                expected,
321                actual,
322                operation,
323            } => {
324                write!(
325                    f,
326                    "Dimension mismatch in {}: expected {}, got {}",
327                    operation, expected, actual
328                )
329            }
330            SolverError::UnsupportedMatrixFormat {
331                current_format,
332                required_format,
333                operation,
334            } => {
335                write!(
336                    f,
337                    "Operation '{}' requires {} format, but matrix is in {} format",
338                    operation, required_format, current_format
339                )
340            }
341            SolverError::MemoryAllocationError {
342                requested_size,
343                available_memory,
344            } => match available_memory {
345                Some(available) => write!(
346                    f,
347                    "Memory allocation failed: requested {} bytes, {} available",
348                    requested_size, available
349                ),
350                None => write!(
351                    f,
352                    "Memory allocation failed: requested {} bytes",
353                    requested_size
354                ),
355            },
356            SolverError::IndexOutOfBounds {
357                index,
358                max_index,
359                context,
360            } => {
361                write!(
362                    f,
363                    "Index {} out of bounds in {}: maximum valid index is {}",
364                    index, context, max_index
365                )
366            }
367            SolverError::InvalidSparseMatrix { reason, position } => match position {
368                Some((row, col)) => {
369                    write!(f, "Invalid sparse matrix at ({}, {}): {}", row, col, reason)
370                }
371                None => write!(f, "Invalid sparse matrix: {}", reason),
372            },
373            SolverError::AlgorithmError {
374                algorithm, message, ..
375            } => {
376                write!(f, "Algorithm '{}' error: {}", algorithm, message)
377            }
378            #[cfg(feature = "wasm")]
379            SolverError::WasmBindingError { message, js_error } => match js_error {
380                Some(js_err) => write!(f, "WASM binding error: {} (JS: {})", message, js_err),
381                None => write!(f, "WASM binding error: {}", message),
382            },
383            #[cfg(feature = "std")]
384            SolverError::IoError { message, context } => {
385                write!(f, "I/O error in {}: {}", context, message)
386            }
387            #[cfg(feature = "serde")]
388            SolverError::SerializationError { message, data_type } => {
389                write!(f, "Serialization error for {}: {}", data_type, message)
390            }
391        }
392    }
393}
394
395#[cfg(feature = "std")]
396impl std::error::Error for SolverError {
397    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
398        None
399    }
400}
401
402// Conversion from standard library errors
403#[cfg(feature = "std")]
404impl From<std::io::Error> for SolverError {
405    fn from(err: std::io::Error) -> Self {
406        SolverError::IoError {
407            message: err.to_string(),
408            context: "File operation".to_string(),
409        }
410    }
411}
412
413// Conversion for WASM environments
414#[cfg(feature = "wasm")]
415impl From<wasm_bindgen::JsValue> for SolverError {
416    fn from(err: wasm_bindgen::JsValue) -> Self {
417        let message = if let Some(string) = err.as_string() {
418            string
419        } else {
420            "Unknown JavaScript error".to_string()
421        };
422
423        SolverError::WasmBindingError {
424            message,
425            js_error: None,
426        }
427    }
428}
429
430#[cfg(all(test, feature = "std"))]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_error_recoverability() {
436        let convergence_error = SolverError::ConvergenceFailure {
437            iterations: 100,
438            residual_norm: 1e-3,
439            tolerance: 1e-6,
440            algorithm: "neumann".to_string(),
441        };
442        assert!(convergence_error.is_recoverable());
443
444        let dimension_error = SolverError::DimensionMismatch {
445            expected: 100,
446            actual: 50,
447            operation: "matrix_vector_multiply".to_string(),
448        };
449        assert!(!dimension_error.is_recoverable());
450    }
451
452    #[test]
453    fn test_recovery_strategies() {
454        let error = SolverError::ConvergenceFailure {
455            iterations: 100,
456            residual_norm: 1e-3,
457            tolerance: 1e-6,
458            algorithm: "neumann".to_string(),
459        };
460
461        if let Some(RecoveryStrategy::SwitchAlgorithm(algo)) = error.recovery_strategy() {
462            assert_eq!(algo, "hybrid");
463        } else {
464            panic!("Expected SwitchAlgorithm recovery strategy");
465        }
466    }
467
468    #[test]
469    fn test_error_severity() {
470        let memory_error = SolverError::MemoryAllocationError {
471            requested_size: 1000000,
472            available_memory: None,
473        };
474        assert_eq!(memory_error.severity(), ErrorSeverity::Critical);
475
476        let convergence_error = SolverError::ConvergenceFailure {
477            iterations: 100,
478            residual_norm: 1e-3,
479            tolerance: 1e-6,
480            algorithm: "neumann".to_string(),
481        };
482        assert_eq!(convergence_error.severity(), ErrorSeverity::Medium);
483    }
484}