Skip to main content

rivrs_sparse/
error.rs

1//! Error types for the sparse solver pipeline.
2//!
3//! Provides [`SparseError`], the unified error enum for all phases of the
4//! sparse solver (analysis, factorization, solve, and I/O). Implements
5//! [`std::error::Error`] and [`Display`](std::fmt::Display) for integration
6//! with standard Rust error handling.
7//!
8//! `PartialEq` is intentionally not derived because the `NumericalSingularity`
9//! variant contains `f64`, where `NaN != NaN` would cause subtle comparison
10//! bugs. Use `matches!()` for pattern-matching assertions in tests.
11
12use std::fmt;
13
14/// Errors that can occur during sparse solver operations.
15///
16/// Note: `PartialEq` is intentionally not derived because the `NumericalSingularity`
17/// variant contains `f64`, where `NaN != NaN` would cause subtle comparison bugs.
18/// Use `matches!()` for pattern-matching assertions in tests.
19#[derive(Debug, Clone)]
20pub enum SparseError {
21    /// Matrix dimensions are incompatible.
22    DimensionMismatch {
23        /// Expected dimensions (rows, cols).
24        expected: (usize, usize),
25        /// Actual dimensions (rows, cols).
26        got: (usize, usize),
27        /// Description of where the mismatch occurred.
28        context: String,
29    },
30
31    /// A matrix that should be square is not.
32    NotSquare {
33        /// Actual dimensions (rows, cols).
34        dims: (usize, usize),
35    },
36
37    /// Input contains NaN, Inf, or other invalid floating-point values.
38    InvalidInput {
39        /// Description of the invalid input.
40        reason: String,
41    },
42
43    /// The matrix is structurally singular (symbolic analysis detected zero diagonal).
44    StructurallySingular {
45        /// Zero-diagonal column index.
46        column: usize,
47    },
48
49    /// Numeric factorization encountered a zero or near-zero pivot.
50    NumericalSingularity {
51        /// Index of the singular pivot.
52        pivot_index: usize,
53        /// The near-zero pivot value.
54        value: f64,
55    },
56
57    /// An ordering or analysis algorithm failed.
58    AnalysisFailure {
59        /// Description of the failure.
60        reason: String,
61    },
62
63    /// An IO operation failed (file not found, permission denied, etc.).
64    IoError {
65        /// Error description.
66        source: String,
67        /// File path that caused the error.
68        path: String,
69    },
70
71    /// A file could not be parsed (malformed Matrix Market, invalid JSON, etc.).
72    ParseError {
73        /// Description of the parse failure.
74        reason: String,
75        /// File path that could not be parsed.
76        path: String,
77        /// Line number where parsing failed, if available.
78        line: Option<usize>,
79    },
80
81    /// A named matrix was not found in the registry.
82    MatrixNotFound {
83        /// Name that was looked up.
84        name: String,
85    },
86
87    /// Solve was called before factor() completed.
88    SolveBeforeFactor {
89        /// Description of the premature solve attempt.
90        context: String,
91    },
92}
93
94impl fmt::Display for SparseError {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Self::DimensionMismatch {
98                expected,
99                got,
100                context,
101            } => {
102                write!(
103                    f,
104                    "Dimension mismatch: expected {:?}, got {:?}. {}",
105                    expected, got, context
106                )
107            }
108            Self::NotSquare { dims } => {
109                write!(
110                    f,
111                    "Matrix must be square, but has dimensions {}x{}",
112                    dims.0, dims.1
113                )
114            }
115            Self::InvalidInput { reason } => {
116                write!(f, "Invalid input: {}", reason)
117            }
118            Self::StructurallySingular { column } => {
119                write!(f, "Matrix is structurally singular at column {}", column)
120            }
121            Self::NumericalSingularity { pivot_index, value } => {
122                write!(
123                    f,
124                    "Numerical singularity at pivot index {} (value: {:.2e})",
125                    pivot_index, value
126                )
127            }
128            Self::AnalysisFailure { reason } => {
129                write!(f, "Symbolic analysis failed: {}", reason)
130            }
131            Self::IoError { source, path } => {
132                write!(f, "IO error for '{}': {}", path, source)
133            }
134            Self::ParseError { reason, path, line } => {
135                if let Some(line) = line {
136                    write!(f, "Parse error in '{}' at line {}: {}", path, line, reason)
137                } else {
138                    write!(f, "Parse error in '{}': {}", path, reason)
139                }
140            }
141            Self::MatrixNotFound { name } => {
142                write!(f, "Matrix '{}' not found in registry", name)
143            }
144            Self::SolveBeforeFactor { context } => {
145                write!(f, "Solve called before factor(): {}", context)
146            }
147        }
148    }
149}
150
151impl std::error::Error for SparseError {}
152
153// Note: blanket `From<std::io::Error>` and `From<serde_json::Error>` impls were
154// intentionally removed. They produced errors with empty `path` fields, losing
155// context. All call sites should construct `SparseError::IoError` or
156// `SparseError::ParseError` explicitly with the relevant file path.