blob: 1f5aa0f6e47d378f7225e0b00154b9fb6aea96de [file] [view]
# Sparse Matrices And Solvers
Use this guide for [`Eigen/src/SparseCore`](../Eigen/src/SparseCore), the sparse decompositions in `SparseCholesky`,
`SparseLU`, `SparseQR`, and `OrderingMethods`, the external `*Support` backend wrappers, and their tests.
`IterativeLinearSolvers` shares the solver contract below; [`numerics.md`](numerics.md) governs accuracy expectations
for all of them. The checked-out headers are authoritative:
- [`SparseMatrix.h`](../Eigen/src/SparseCore/SparseMatrix.h) owns both storage modes, assembly, and resizing.
- [`SparseCompressedBase.h`](../Eigen/src/SparseCore/SparseCompressedBase.h) exposes the raw arrays, `InnerIterator`,
and the inner-index sorting API.
- [`SparseRef.h`](../Eigen/src/SparseCore/SparseRef.h) and
[`SparsityPatternRef.h`](../Eigen/src/SparseCore/SparsityPatternRef.h) define the non-owning views.
- [`SparseSolverBase.h`](../Eigen/src/SparseCore/SparseSolverBase.h) defines the `solve()` plumbing shared by direct
solvers.
- [`test/sparse.h`](../test/sparse.h) and [`test/sparse_solver.h`](../test/sparse_solver.h) define the shared sparse
test helpers.
## Compressed And Uncompressed Storage
A `SparseMatrix` is in one of two storage modes, and most bugs in this module come from a path that silently assumes
one. `m_innerNonZeros == nullptr` means compressed; when it is non-null, inner vector `j` occupies
`[outerIndexPtr()[j], outerIndexPtr()[j] + innerNonZeroPtr()[j])` rather than running to `outerIndexPtr()[j + 1]`.
- `insert()` and `coeffRef()` turn a compressed matrix into uncompressed mode when they add an entry. A function that
takes a `SparseMatrix&` and inserts is therefore free to change the caller's storage mode; `makeCompressed()`
restores it.
- Do not derive an entry count or an iteration bound from consecutive `outerIndexPtr()` differences. Use `nonZeros()`,
`innerNonZeroPtr()`, or the uniform loop that `SparsityPatternRef.h` documents, which is correct in both modes.
- `resize()` zeroes the matrix, drops to compressed mode, and keeps the allocation; `conservativeResize()` preserves
contents. Neither is a way to change storage mode deliberately.
- `Ref<SparseMatrix>` accepts an uncompressed argument unless it is declared with `StandardCompressedFormat`. With that
option a writable `Ref` asserts `isCompressed()`, while a `Ref<const SparseMatrix, StandardCompressedFormat>`
silently materializes a compressed copy instead of failing. A `Ref` parameter is consequently not proof that no copy
happened; state which form a new API takes and why.
- `InnerIterator` and every raw pointer obtained from the matrix are invalidated by an insertion. Finish iterating, or
collect the coordinates first and mutate afterwards.
- A consumer may require compressed input outright rather than adapting: `SparseQR::analyzePattern` opens with an
`eigen_assert(mat.isCompressed())`, while `SparseLU` branches on `isCompressed()` and falls back to copying the outer
index array. Because that check is an `eigen_assert`, a release build does not diagnose the violation at all — call
`makeCompressed()` before handing an assembled matrix to a direct solver rather than relying on the assertion.
## Sorted Inner Indices
Sorted inner indices within each inner vector are an invariant most of the API maintains and several parts require.
`coeff()` resolves an entry by binary search over the inner range, and the sparse-sparse coefficient-wise binary
evaluator merges two inner streams by advancing whichever index is smaller. Both return wrong values on unsorted
input rather than failing.
The invariant is not universal, and the exception is a trap. `SparseQR::matrixR()` hands back a reference to a stored
factor built with `insertBackByOuterInnerUnordered`, so it is compressed but **not** sorted, and the rank-deficient
path preserves that: right-multiplying a column-major matrix by the pivot permutation takes the outer-permutation
branch, which relocates whole inner vectors without reordering within them. Compressed is not sorted, and a factor
returned by reference has had no opportunity to become either.
A storage-order round-trip is the fix, and it works for a reason worth knowing: a cross-order sparse assignment is a
counting transpose that walks the source in outer order, so each destination inner vector receives its entries in
ascending order no matter how the source was ordered. Eigen relies on this internally — `SparseQR::_sort_matrix_Q()`
sorts the stored reflectors exactly that way before `matrixQ()` is materialized into a sparse destination, which is why
`Q` is not a hazard in the way `R` is. Verify with `innerIndicesAreSorted()` rather than assuming a decomposition's
output factor satisfies the invariant.
- `setFromTriplets()` accepts unsorted input with duplicates and produces a sorted, compressed matrix with duplicates
summed. It destroys the previous contents and does not resize — construct or `resize()` the matrix first, since the
dimensions are not inferred from the triplets.
- `setFromSortedTriplets()`, `insertFromTriplets()`, and `insertFromSortedTriplets()` are the other three corners: the
`Sorted` variants promise pre-sorted input, and the `insertFrom` variants merge into existing entries rather than
replacing them. All four take an optional duplicate functor; the default sums.
- `insert()` requires that the entry not already exist. Use `coeffRef()` when it may, and `reserve(const SizesType&)`
before random-order insertion — the sequential fast path only holds for increasing outer indices.
- The sparse-sparse product selectors preserve sortedness deliberately, choosing between a sorted insertion and an
unsorted pass followed by a transpose round-trip that sorts as a side effect
([`ConservativeSparseSparseProduct.h`](../Eigen/src/SparseCore/ConservativeSparseSparseProduct.h)). A new product,
permutation, or assembly path must restore the invariant; `sortInnerIndices()` and `innerIndicesAreSorted()` on
`SparseCompressedBase` are the tools, and the latter belongs in a test rather than only in reasoning.
## Products
`A * B` on two sparse operands uses the conservative product; `(A * B).pruned()` selects the pruning product in
[`SparseSparseProductWithPruning.h`](../Eigen/src/SparseCore/SparseSparseProductWithPruning.h) instead. The distinction
is semantic before it is a matter of performance: the conservative path stores every structurally generated entry,
including one whose accumulation cancels to exactly zero, while the pruning path drops completed values at or below its
tolerance. The two therefore produce different patterns from the same operands, and a test or benchmark written against
one does not transfer to the other. Neither reserves the exact result size up front — the conservative path starts from
the heuristic `nonZerosEstimate()` sum documented at its definition and grows or over-allocates from there.
Threaded SpMV is opt-in: [`Eigen/SparseCore`](../Eigen/SparseCore) includes
`ThreadedSparseProduct.h` and `Eigen/ThreadPool` only under `EIGEN_USE_THREADS`. Coverage lives in
`test/sparse_threaded_product.cpp`, and [`tensor-threadpool.md`](tensor-threadpool.md) applies to its threading.
## Solver Contract
Direct sparse solvers split pattern analysis from numerical work: `analyzePattern()`, then `factorize()`, with
`compute()` doing both. Re-solving with the same pattern and new values must reuse the analysis; a change that forces a
re-analysis is a performance regression even when results match.
- Sparse solvers report failure through `info()`, not exceptions, and `info()` lives on each concrete solver and on
`IterativeSolverBase` — not on `SparseSolverBase`. Check it after `compute()`/`factorize()` and again after `solve()`
where the solver documents doing so. A test that ignores `info()` can pass on a matrix the solver rejected.
- `solve()` asserts that the solver was initialized, so a missing `compute()` surfaces only in a debug build.
- Reordering is part of the result: a solver's permutation affects fill-in and the achievable accuracy, so an ordering
change needs the fill-in or timing evidence [`benchmarking.md`](benchmarking.md) asks for, not only a residual check.
## Testing Sparse Changes
`initSparse()` in `test/sparse.h` fills a dense reference and a sparse matrix together, with `ForceNonZeroDiag`,
`MakeLowerTriangular`, `MakeUpperTriangular`, and `ForceRealDiag` for the shapes solvers require. `test/sparse_solver.h`
provides the `check_sparse_solving`, `check_sparse_spd_solving`, `check_sparse_nonhermitian_solving`, and determinant
harnesses; prefer them to a hand-rolled solve so a new solver inherits the established coverage.
Scale coverage to the axes this module actually branches on: both storage orders, **both storage modes**, a
non-default `StorageIndex` width, complex scalars where conjugation is not a no-op, and a matrix with an empty inner
vector. Comparing against a dense reference computed by Eigen is the standard technique; keep the tolerance a named
epsilon multiple scaled by dimension or conditioning as [`testing.md`](testing.md) requires.
External backend tests are registered conditionally in [`test/CMakeLists.txt`](../test/CMakeLists.txt), so a green local
run says nothing about any of them. The full set is `cholmod_support`, `umfpack_support`, `klu_support`,
`superlu_support`, `pastix_support`, `spqr_support`, `accelerate_support`, `metis_support` — an ordering backend rather
than a solver — and `pardiso_support`. Most are gated on a `find_package` result and register in
`EIGEN_MISSING_BACKENDS` when absent, several additionally require `EIGEN_BUILD_BLAS` or `EIGEN_BUILD_LAPACK`, and
`metis_support` and `pastix_support` depend on variables the PaStiX search sets when the `METIS` component is requested.
`pardiso_support` is the exception worth knowing: the tree contains no `find_package(PARDISO)` and no
`EIGEN_MISSING_BACKENDS` entry for it, so it is registered only when `PARDISO_FOUND` arrives from outside the project
and its absence is silent even in the missing-backend summary. Report which sparse backends were unavailable rather than
implying full coverage.