Skip to main content

rivrs_sparse/
lib.rs

1#![warn(missing_docs)]
2//! rivrs-sparse — Sparse Linear Algebra
3//!
4//! This library heavily relies on [`faer`](https://crates.io/crates/faer) for
5//! foundational data types (e.g., `SparseColMat`, `Triplet`, `Col`), dense
6//! solvers, and some sparse linear algebra routines (e.g., AMD ordering).
7//!
8//! # Sparse Symmetric Indefinite Solver
9//!
10//! The library provides a sparse symmetric indefinite direct solver
11//! based on the A Posteriori Threshold Pivoting (APTP) algorithm:
12//!
13//! - **Symbolic analysis**: Ordering, elimination tree, symbolic factorization
14//! - **Numeric factorization**: LDL^T with APTP pivoting
15//! - **Triangular solve**: Forward/backward substitution
16//!
17//! The primary references are:
18//!
19//! - Hogg, Duff, & Lopez (2020), "A New Sparse LDL^T Solver Using
20//!   A Posteriori Threshold Pivoting", SIAM J. Sci. Comput.
21//! - Duff & Reid (1983), "The multifrontal solution of indefinite sparse
22//!   symmetric linear equations"
23//! - Liu (1992), "The Multifrontal Method for Sparse Matrix Solution:
24//!   Theory and Practice", SIAM Review
25//! - SPRAL (BSD-3-Clause) for reference implementation patterns
26//!
27//! See [NOTICE](../NOTICE) for complete attribution and licensing information.
28//!
29//! # Quick Start
30//!
31//! ```
32//! use faer::sparse::{SparseColMat, Triplet};
33//! use faer::Col;
34//! use rivrs_sparse::symmetric::{SparseLDLT, SolverOptions};
35//!
36//! // Symmetric indefinite 2x2: A = [[2, 1], [1, -1]]
37//! let triplets = vec![
38//!     Triplet::new(0, 0, 2.0),
39//!     Triplet::new(1, 0, 1.0), Triplet::new(0, 1, 1.0),
40//!     Triplet::new(1, 1, -1.0),
41//! ];
42//! let a = SparseColMat::try_new_from_triplets(2, 2, &triplets).unwrap();
43//! let b = Col::from_fn(2, |i| [3.0, 0.0][i]);
44//!
45//! let x = SparseLDLT::solve_full(&a, &b, &SolverOptions::default()).unwrap();
46//! assert!((x[0] - 1.0).abs() < 1e-12);
47//! assert!((x[1] - 1.0).abs() < 1e-12);
48//! ```
49//!
50//! # Three-Step Solver API
51//!
52//! For repeated solves with the same matrix or sparsity pattern, use the three-step
53//! solve API:
54//!
55//! 1. Symbolic analysis (reusable for same sparsity pattern)
56//! 2. Numeric factorization (reusable for same matrix)
57//! 3. Triangular solve
58//!
59//! For instance, with multiple right-hand side vectors but the same matrix,
60//! perform steps 1-2 once and then reuse the factorization for each vector.
61//!
62//! ```
63//! use faer::sparse::{SparseColMat, Triplet};
64//! use faer::{Col, Par};
65//! use faer::dyn_stack::{MemBuffer, MemStack};
66//! use rivrs_sparse::symmetric::{SparseLDLT, AnalyzeOptions, FactorOptions};
67//!
68//! // Build a symmetric matrix (full CSC — both triangles stored)
69//! let triplets = vec![
70//!     Triplet::new(0, 0, 4.0),
71//!     Triplet::new(1, 0, 1.0), Triplet::new(0, 1, 1.0),
72//!     Triplet::new(1, 1, 3.0),
73//! ];
74//! let a = SparseColMat::try_new_from_triplets(2, 2, &triplets).unwrap();
75//!
76//! // Phase 1: Symbolic analysis
77//! let mut solver = SparseLDLT::analyze_with_matrix(&a, &AnalyzeOptions::default())?;
78//!
79//! // Phase 2: Numeric factorization
80//! solver.factor(&a, &FactorOptions::default())?;
81//!
82//! // Phase 3: Triangular solve
83//! let b = Col::from_fn(2, |i| [5.0, 4.0][i]);
84//! let req = solver.solve_scratch(1);
85//! let mut mem = MemBuffer::new(req);
86//! let x = solver.solve(&b, &mut MemStack::new(&mut mem), Par::Seq)?;
87//! assert!((x[0] - 1.0).abs() < 1e-12);
88//! assert!((x[1] - 1.0).abs() < 1e-12);
89//! # Ok::<(), rivrs_sparse::error::SparseError>(())
90//! ```
91//!
92//! # Ordering Strategies
93//!
94//! | Strategy | Best for | Notes |
95//! |----------|----------|-------|
96//! | [`MatchOrderMetis`](symmetric::OrderingStrategy::MatchOrderMetis) | Hard indefinite (KKT, saddle-point) | Default. MC64 matching + METIS |
97//! | [`Metis`](symmetric::OrderingStrategy::Metis) | Easy indefinite (FEM, thermal) | Pure METIS nested dissection |
98//! | [`Amd`](symmetric::OrderingStrategy::Amd) | Small matrices, unit tests | faer built-in AMD |
99//!
100//! # Feature Flags
101//!
102//! | Feature | Purpose |
103//! |---------|---------|
104//! | `diagnostic` | Per-supernode timing instrumentation. Zero overhead when disabled. |
105//! | `test-util` | Test infrastructure: random matrix generators, property-based testing. |
106//!
107//! # Error Handling
108//!
109//! All fallible operations return [`Result<T, SparseError>`](error::SparseError).
110//! Error variants are organized by solver phase:
111//!
112//! - **Analysis**: [`NotSquare`](error::SparseError::NotSquare),
113//!   [`AnalysisFailure`](error::SparseError::AnalysisFailure)
114//! - **Factorization**: [`NumericalSingularity`](error::SparseError::NumericalSingularity),
115//!   [`StructurallySingular`](error::SparseError::StructurallySingular)
116//! - **Solve**: [`SolveBeforeFactor`](error::SparseError::SolveBeforeFactor),
117//!   [`DimensionMismatch`](error::SparseError::DimensionMismatch)
118//! - **I/O**: [`IoError`](error::SparseError::IoError),
119//!   [`ParseError`](error::SparseError::ParseError)
120//!
121//! # Matrix Storage Convention
122//!
123//! All input matrices must be stored as **full symmetric CSC** (both upper and
124//! lower triangles). The [`io::mtx`] reader automatically mirrors entries from
125//! Matrix Market files.
126//!
127//! # Parallelism
128//!
129//! Both factorization and solve support shared-memory parallelism via
130//! `Par::Seq` (sequential) or `Par::rayon(n)` (parallel with `n` threads):
131//!
132//! ```no_run
133//! # use faer::Par;
134//! # use rivrs_sparse::symmetric::FactorOptions;
135//! let opts = FactorOptions { par: Par::rayon(4), ..Default::default() };
136//! ```
137//!
138//! Tree-level parallelism (independent subtrees via rayon) and intra-node
139//! parallelism (TRSM/GEMM via faer `Par`) are both controlled by this setting.
140//!
141//! # Performance
142//!
143//! Factorization dominates total solve time for most matrices. On the
144//! 65-matrix SuiteSparse benchmark suite, rivrs-sparse is competitive with
145//! SPRAL (median 5% faster sequential, 10% faster at 8 threads). Tuning
146//! [`FactorOptions::threshold`](symmetric::FactorOptions::threshold) (default
147//! 0.01) trades off stability vs fill-in for specific problem classes.
148//! See the [repository](https://github.com/pinetreelabs/rivrs-linalg/sparse) for
149//! benchmarking details.
150//!
151//! # Examples
152//!
153//! See the [`examples/`](https://github.com/pinetreelabs/rivrs-linalg/tree/main/sparse/examples)
154//! directory for complete programs:
155//!
156//! - `basic_usage.rs` — Self-contained hello world
157//! - `multiple_rhs.rs` — Solve for multiple right-hand sides, reusing the factorization
158//! - `refactorization.rs` — Refactorize with different values on the same sparsity pattern
159//! - `solve_timing.rs` — End-to-end solve timing on SuiteSparse matrices
160//! - `profile_matrix.rs` — Per-supernode profiling with Chrome Trace export
161//! - `parallel_scaling.rs` — Parallel speedup measurement
162
163use std::fmt;
164
165use serde::{Deserialize, Serialize};
166
167pub mod error;
168pub mod io;
169pub mod ordering;
170pub mod symmetric;
171pub mod validate;
172
173#[cfg(any(feature = "test-util", feature = "diagnostic"))]
174pub mod benchmarking;
175#[cfg(feature = "test-util")]
176pub mod debug;
177#[cfg(any(feature = "test-util", feature = "diagnostic"))]
178pub mod profiling;
179#[cfg(feature = "test-util")]
180pub mod testing;
181
182/// Solver phases corresponding to the three-phase API: analyze → factorize → solve.
183///
184/// Variants are ordered to match the natural pipeline order. `Roundtrip` represents
185/// the full end-to-end pipeline.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum SolverPhase {
189    /// Symbolic analysis: ordering, elimination tree, supernode structure.
190    Analyze,
191    /// Numeric factorization: LDL^T with APTP pivoting.
192    Factor,
193    /// Triangular solve: forward/backward substitution.
194    Solve,
195    /// Full end-to-end pipeline (analyze + factor + solve).
196    Roundtrip,
197}
198
199impl fmt::Display for SolverPhase {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Self::Analyze => write!(f, "analyze"),
203            Self::Factor => write!(f, "factor"),
204            Self::Solve => write!(f, "solve"),
205            Self::Roundtrip => write!(f, "roundtrip"),
206        }
207    }
208}
209
210impl SolverPhase {
211    /// All individual component phases (excludes Roundtrip).
212    pub const fn components() -> &'static [Self] {
213        &[Self::Analyze, Self::Factor, Self::Solve]
214    }
215
216    /// All phases including Roundtrip.
217    pub const fn all() -> &'static [Self] {
218        &[Self::Analyze, Self::Factor, Self::Solve, Self::Roundtrip]
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn phase_display() {
228        assert_eq!(SolverPhase::Analyze.to_string(), "analyze");
229        assert_eq!(SolverPhase::Factor.to_string(), "factor");
230        assert_eq!(SolverPhase::Solve.to_string(), "solve");
231        assert_eq!(SolverPhase::Roundtrip.to_string(), "roundtrip");
232    }
233
234    #[test]
235    fn phase_serde_roundtrip() {
236        for phase in SolverPhase::all() {
237            let json = serde_json::to_string(phase).unwrap();
238            let back: SolverPhase = serde_json::from_str(&json).unwrap();
239            assert_eq!(*phase, back);
240        }
241    }
242
243    #[test]
244    fn phase_ordering_matches_pipeline() {
245        assert!(SolverPhase::Analyze < SolverPhase::Factor);
246        assert!(SolverPhase::Factor < SolverPhase::Solve);
247        assert!(SolverPhase::Solve < SolverPhase::Roundtrip);
248    }
249}