blob: 62a1e7584722b5e0e631cb6106b405a8cdbb419e [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) implements 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, compressed or uncompressed, and most bugs in this module come from code
that silently assumes one of the two. The matrix is compressed when `m_innerNonZeros == nullptr`. 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. So a function
that takes a `SparseMatrix&` and inserts can leave the caller's matrix uncompressed; `makeCompressed()` restores
compressed mode.
- 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. That loop 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 makes a compressed copy instead of failing. So a `Ref` parameter does not prove that no copy was made. For a
new API, state which form it takes and why.
- An insertion invalidates `InnerIterator` and every raw pointer obtained from the matrix. Finish iterating, or collect
the coordinates first and mutate afterwards.
- Before passing an assembled matrix to a direct solver, call `makeCompressed()` rather than relying on an assertion.
Some consumers require compressed input instead of handling both modes. `SparseQR::analyzePattern` starts with
`eigen_assert(mat.isCompressed())`, while `SparseLU` branches on `isCompressed()` and falls back to copying the outer
index array. Because the `SparseQR` check is an `eigen_assert`, a release build does not report the violation at all.
## Sorted Inner Indices
Most of the API keeps the inner indices of each inner vector sorted, and several parts require it. `coeff()` finds an
entry by binary search over the inner range. The evaluator for a coefficient-wise operation on two sparse operands,
such as `A + B`, merges the two inner vectors by advancing whichever index is smaller. On unsorted input both return
wrong values instead of failing.
Sorted order is not guaranteed everywhere, and the exception is easy to miss. `SparseQR::matrixR()` returns a reference
to a stored factor built with `insertBackByOuterInnerUnordered`, so it is compressed but **not** sorted. The
rank-deficient path keeps it unsorted: it right-multiplies a column-major matrix by the pivot permutation, and that
product takes the outer-permutation branch, which moves whole inner vectors without reordering the entries inside them.
A compressed matrix is not necessarily sorted, and a factor returned by reference has not been through an assignment
that would compress or sort it. Check a decomposition's output factor with `innerIndicesAreSorted()` rather than
assuming it is sorted.
To sort such a matrix, assign it to a matrix of the other storage order and back (a storage-order round-trip). This
works because an assignment between storage orders is a counting transpose: it walks the source in outer order, so
each destination inner vector receives its entries in ascending order however the source was ordered. Eigen relies on
this internally. `SparseQR::_sort_matrix_Q()` sorts the stored reflectors this way before `matrixQ()` is materialized
into a sparse destination. So `Q`, unlike `R`, comes out sorted.
- `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
`setFromTriplets()` does not infer the dimensions from the triplets.
- `setFromSortedTriplets()`, `insertFromTriplets()`, and `insertFromSortedTriplets()` complete the set. The `Sorted`
variants assume the input is already sorted, and the `insertFrom` variants merge into existing entries rather than
replacing them. All four take an optional functor for combining duplicates; the default sums them.
- `insert()` requires that the entry not already exist. Use `coeffRef()` when it might exist. Before inserting in random
order, call `reserve(const SizesType&)`: the sequential fast path applies only when outer indices increase.
- The sparse-sparse product selectors keep the result sorted on purpose. They choose between inserting in sorted order
and an unsorted pass followed by a transpose round-trip, which sorts as a side effect
([`ConservativeSparseSparseProduct.h`](../Eigen/src/SparseCore/ConservativeSparseSparseProduct.h)). A new product,
permutation, or assembly path must also leave the indices sorted. `sortInnerIndices()` and `innerIndicesAreSorted()`
on `SparseCompressedBase` are the tools for this. Check `innerIndicesAreSorted()` in a test, not 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 two
differ in results, not only in speed. The conservative product stores every entry that the operands' sparsity patterns
generate, even one whose sum cancels to exactly zero. The pruning product drops finished values at or below its
tolerance. So the two produce different patterns from the same operands, and a test or benchmark written against one
does not transfer to the other.
Neither product reserves the exact result size in advance. The conservative product 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 when `EIGEN_USE_THREADS` is defined. Its tests are 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. `info()` is a member of each concrete solver and of
`IterativeSolverBase`, not of `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. For an ordering
change, provide 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 that 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.
[`test/CMakeLists.txt`](../test/CMakeLists.txt) registers the external backend tests conditionally, 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 registered only if a `find_package` call finds the library; otherwise
the backend is added to `EIGEN_MISSING_BACKENDS`. Several also require `EIGEN_BUILD_BLAS` or `EIGEN_BUILD_LAPACK`.
`metis_support` and `pastix_support` depend on variables that the PaStiX search sets when the `METIS` component is
requested.
`pardiso_support` is the exception to know about. The tree contains no `find_package(PARDISO)` and no
`EIGEN_MISSING_BACKENDS` entry for it. So it is registered only when `PARDISO_FOUND` is set from outside the project,
and nothing reports its absence, not even the missing-backend summary.
Report which sparse backends were unavailable rather than implying full coverage.