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 core::fmt;
8use alloc::{string::String, vec::Vec};
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" => RecoveryStrategy::SwitchAlgorithm("backward_push".to_string()),
196                    "backward_push" => RecoveryStrategy::SwitchAlgorithm("hybrid".to_string()),
197                    _ => RecoveryStrategy::RelaxTolerance(10.0),
198                })
199            },
200            SolverError::NumericalInstability { .. } => {
201                Some(RecoveryStrategy::IncreasePrecision)
202            },
203            SolverError::UnsupportedMatrixFormat { required_format, .. } => {
204                Some(RecoveryStrategy::ConvertMatrixFormat(required_format.clone()))
205            },
206            SolverError::AlgorithmError { algorithm, .. } => {
207                Some(RecoveryStrategy::SwitchAlgorithm("neumann".to_string()))
208            },
209            _ => None,
210        }
211    }
212    
213    /// Get the error severity level.
214    pub fn severity(&self) -> ErrorSeverity {
215        match self {
216            SolverError::MemoryAllocationError { .. } => ErrorSeverity::Critical,
217            SolverError::InvalidSparseMatrix { .. } => ErrorSeverity::Critical,
218            SolverError::IndexOutOfBounds { .. } => ErrorSeverity::Critical,
219            SolverError::MatrixNotDiagonallyDominant { .. } => ErrorSeverity::High,
220            SolverError::ConvergenceFailure { .. } => ErrorSeverity::Medium,
221            SolverError::NumericalInstability { .. } => ErrorSeverity::Medium,
222            // Incoherent is a *budget* refusal, not a data corruption — Low.
223            // The caller asked us to refuse, we refused; nothing's broken.
224            SolverError::Incoherent { .. } => ErrorSeverity::Low,
225            SolverError::InvalidInput { .. } => ErrorSeverity::Medium,
226            SolverError::DimensionMismatch { .. } => ErrorSeverity::Medium,
227            SolverError::UnsupportedMatrixFormat { .. } => ErrorSeverity::Low,
228            SolverError::AlgorithmError { .. } => ErrorSeverity::Medium,
229            #[cfg(feature = "wasm")]
230            SolverError::WasmBindingError { .. } => ErrorSeverity::High,
231            #[cfg(feature = "std")]
232            SolverError::IoError { .. } => ErrorSeverity::Medium,
233            #[cfg(feature = "serde")]
234            SolverError::SerializationError { .. } => ErrorSeverity::Low,
235        }
236    }
237}
238
239/// Recovery strategies for recoverable errors.
240#[derive(Debug, Clone, PartialEq)]
241#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
242pub enum RecoveryStrategy {
243    /// Switch to a different solver algorithm.
244    SwitchAlgorithm(String),
245    /// Increase numerical precision (f32 -> f64).
246    IncreasePrecision,
247    /// Relax convergence tolerance by the given factor.
248    RelaxTolerance(f64),
249    /// Restart with different random seed.
250    RestartWithDifferentSeed,
251    /// Convert matrix to a different storage format.
252    ConvertMatrixFormat(String),
253    /// Increase maximum iteration count.
254    IncreaseIterations(usize),
255}
256
257/// Error severity levels for logging and monitoring.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260pub enum ErrorSeverity {
261    /// Low severity - algorithm can continue with degraded performance
262    Low,
263    /// Medium severity - operation failed but system remains stable
264    Medium,
265    /// High severity - significant failure requiring user intervention
266    High,
267    /// Critical severity - system integrity compromised
268    Critical,
269}
270
271impl fmt::Display for SolverError {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match self {
274            SolverError::MatrixNotDiagonallyDominant { row, diagonal, off_diagonal_sum } => {
275                write!(f, "Matrix is not diagonally dominant at row {}: diagonal = {:.6}, off-diagonal sum = {:.6}", 
276                       row, diagonal, off_diagonal_sum)
277            },
278            SolverError::NumericalInstability { reason, iteration, residual_norm } => {
279                write!(f, "Numerical instability at iteration {}: {} (residual = {:.2e})", 
280                       iteration, reason, residual_norm)
281            },
282            SolverError::ConvergenceFailure { iterations, residual_norm, tolerance, algorithm } => {
283                write!(f, "Algorithm '{}' failed to converge after {} iterations: residual = {:.2e} > tolerance = {:.2e}",
284                       algorithm, iterations, residual_norm, tolerance)
285            },
286            SolverError::Incoherent { coherence, threshold } => {
287                write!(f,
288                    "Coherence gate refused solve: matrix coherence = {:.6} < threshold = {:.6} \
289                     (ADR-001 item #3 — set SolverOptions::coherence_threshold to 0.0 to disable)",
290                    coherence, threshold,
291                )
292            },
293            SolverError::InvalidInput { message, parameter } => {
294                match parameter {
295                    Some(param) => write!(f, "Invalid input for parameter '{}': {}", param, message),
296                    None => write!(f, "Invalid input: {}", message),
297                }
298            },
299            SolverError::DimensionMismatch { expected, actual, operation } => {
300                write!(f, "Dimension mismatch in {}: expected {}, got {}", operation, expected, actual)
301            },
302            SolverError::UnsupportedMatrixFormat { current_format, required_format, operation } => {
303                write!(f, "Operation '{}' requires {} format, but matrix is in {} format", 
304                       operation, required_format, current_format)
305            },
306            SolverError::MemoryAllocationError { requested_size, available_memory } => {
307                match available_memory {
308                    Some(available) => write!(f, "Memory allocation failed: requested {} bytes, {} available", 
309                                             requested_size, available),
310                    None => write!(f, "Memory allocation failed: requested {} bytes", requested_size),
311                }
312            },
313            SolverError::IndexOutOfBounds { index, max_index, context } => {
314                write!(f, "Index {} out of bounds in {}: maximum valid index is {}", 
315                       index, context, max_index)
316            },
317            SolverError::InvalidSparseMatrix { reason, position } => {
318                match position {
319                    Some((row, col)) => write!(f, "Invalid sparse matrix at ({}, {}): {}", row, col, reason),
320                    None => write!(f, "Invalid sparse matrix: {}", reason),
321                }
322            },
323            SolverError::AlgorithmError { algorithm, message, .. } => {
324                write!(f, "Algorithm '{}' error: {}", algorithm, message)
325            },
326            #[cfg(feature = "wasm")]
327            SolverError::WasmBindingError { message, js_error } => {
328                match js_error {
329                    Some(js_err) => write!(f, "WASM binding error: {} (JS: {})", message, js_err),
330                    None => write!(f, "WASM binding error: {}", message),
331                }
332            },
333            #[cfg(feature = "std")]
334            SolverError::IoError { message, context } => {
335                write!(f, "I/O error in {}: {}", context, message)
336            },
337            #[cfg(feature = "serde")]
338            SolverError::SerializationError { message, data_type } => {
339                write!(f, "Serialization error for {}: {}", data_type, message)
340            },
341        }
342    }
343}
344
345#[cfg(feature = "std")]
346impl std::error::Error for SolverError {
347    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
348        None
349    }
350}
351
352// Conversion from standard library errors
353#[cfg(feature = "std")]
354impl From<std::io::Error> for SolverError {
355    fn from(err: std::io::Error) -> Self {
356        SolverError::IoError {
357            message: err.to_string(),
358            context: "File operation".to_string(),
359        }
360    }
361}
362
363// Conversion for WASM environments
364#[cfg(feature = "wasm")]
365impl From<wasm_bindgen::JsValue> for SolverError {
366    fn from(err: wasm_bindgen::JsValue) -> Self {
367        let message = if let Some(string) = err.as_string() {
368            string
369        } else {
370            "Unknown JavaScript error".to_string()
371        };
372        
373        SolverError::WasmBindingError {
374            message,
375            js_error: None,
376        }
377    }
378}
379
380#[cfg(all(test, feature = "std"))]
381mod tests {
382    use super::*;
383    
384    #[test]
385    fn test_error_recoverability() {
386        let convergence_error = SolverError::ConvergenceFailure {
387            iterations: 100,
388            residual_norm: 1e-3,
389            tolerance: 1e-6,
390            algorithm: "neumann".to_string(),
391        };
392        assert!(convergence_error.is_recoverable());
393        
394        let dimension_error = SolverError::DimensionMismatch {
395            expected: 100,
396            actual: 50,
397            operation: "matrix_vector_multiply".to_string(),
398        };
399        assert!(!dimension_error.is_recoverable());
400    }
401    
402    #[test]
403    fn test_recovery_strategies() {
404        let error = SolverError::ConvergenceFailure {
405            iterations: 100,
406            residual_norm: 1e-3,
407            tolerance: 1e-6,
408            algorithm: "neumann".to_string(),
409        };
410        
411        if let Some(RecoveryStrategy::SwitchAlgorithm(algo)) = error.recovery_strategy() {
412            assert_eq!(algo, "hybrid");
413        } else {
414            panic!("Expected SwitchAlgorithm recovery strategy");
415        }
416    }
417    
418    #[test]
419    fn test_error_severity() {
420        let memory_error = SolverError::MemoryAllocationError {
421            requested_size: 1000000,
422            available_memory: None,
423        };
424        assert_eq!(memory_error.severity(), ErrorSeverity::Critical);
425        
426        let convergence_error = SolverError::ConvergenceFailure {
427            iterations: 100,
428            residual_norm: 1e-3,
429            tolerance: 1e-6,
430            algorithm: "neumann".to_string(),
431        };
432        assert_eq!(convergence_error.severity(), ErrorSeverity::Medium);
433    }
434}