Reach-based sparse triangular solveInPlace (Gilbert–Peierls) for col-major L / Lᵀ
libeigen/eigen!2678
This commit is contained in:
committed by
Rasmus Munk Larsen
parent
0af925cb01
commit
70034bc005
@@ -58,6 +58,7 @@
|
||||
#include "src/SparseCore/SparseDenseProduct.h"
|
||||
#include "src/SparseCore/SparseSelfAdjointView.h"
|
||||
#include "src/SparseCore/SparseTriangularView.h"
|
||||
#include "src/SparseCore/TriangularReachSolver.h"
|
||||
#include "src/SparseCore/TriangularSolver.h"
|
||||
#include "src/SparseCore/SparsePermutation.h"
|
||||
#include "src/SparseCore/SparseFuzzy.h"
|
||||
|
||||
@@ -61,6 +61,17 @@ class SparseSymmetricPermutationProduct;
|
||||
|
||||
namespace internal {
|
||||
|
||||
/** \internal
|
||||
* Compile-time test for whether a sparse expression exposes its storage directly
|
||||
* through outerIndexPtr() / innerIndexPtr() / valuePtr() / innerNonZeroPtr()
|
||||
* (the \ref CompressedAccessBit contract: SparseMatrix, SparseVector,
|
||||
* Ref<Sparse>, Map<SparseMatrix>, ...). Such expressions can drive raw-pointer
|
||||
* fast paths; anything else must be walked via InnerIterator.
|
||||
*/
|
||||
template <typename Derived>
|
||||
struct has_compressed_access : std::integral_constant<bool, (int(traits<Derived>::Flags) & CompressedAccessBit) != 0> {
|
||||
};
|
||||
|
||||
template <typename T, int Rows, int Cols, int Flags>
|
||||
struct sparse_eval;
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
// This file is part of Eigen, a lightweight C++ template library
|
||||
// for linear algebra.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla
|
||||
// Public License v. 2.0. If a copy of the MPL was not distributed
|
||||
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
// SPDX-FileCopyrightText: The Eigen Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
#ifndef EIGEN_TRIANGULAR_REACH_SOLVER_H
|
||||
#define EIGEN_TRIANGULAR_REACH_SOLVER_H
|
||||
|
||||
// IWYU pragma: private
|
||||
#include "./InternalHeaderCheck.h"
|
||||
|
||||
namespace Eigen {
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Gilbert-Peierls sparse-rhs triangular solve.
|
||||
//
|
||||
// Solves T x = b for a column-major sparse triangular T (lower OR upper) with sorted
|
||||
// columns, and a sparse right-hand side b. The cost is O(|reach| + flops): only the
|
||||
// columns reachable from b's pattern are touched, independent of the dimension.
|
||||
//
|
||||
// T is the triangular VIEW of a possibly-general stored matrix, so a column may hold
|
||||
// entries on BOTH sides of the diagonal; only the entries in the active triangle
|
||||
// participate. Everything is therefore direction-aware via a compile-time bool `Upper`:
|
||||
// - the reach follows only in-triangle successors (strictly below the diagonal for
|
||||
// lower, strictly above for upper -- one comparison per stored entry);
|
||||
// - the numeric sweep locates the diagonal within the sorted column (first entry with
|
||||
// index >= j for lower, last with index <= j for upper) and updates only the
|
||||
// in-triangle off-diagonal entries.
|
||||
// For a genuinely triangular stored matrix (Eigen's SparseMatrix) the in-triangle
|
||||
// entries are the whole column and the diagonal sits at the extreme, so the boundary
|
||||
// scans are single-comparison no-ops.
|
||||
|
||||
// ===========================================================================
|
||||
// Reach: any triangular view, raw CSC storage.
|
||||
// ===========================================================================
|
||||
|
||||
// Computes reach_{G(T)}(pattern(b)) via a non-recursive depth-first search, emitting
|
||||
// the reached columns into xi[top..n) in topological (solve) order and returning top.
|
||||
// `mark` (length-n byte array; a 0/1 visited flag needs no more) must be all-zero on
|
||||
// entry; every reached node is flagged, and since the reached set is exactly the
|
||||
// output, the caller clears those flags again while gathering (no reset needed). `xi`
|
||||
// and `pstack` are size-n scratch; the DFS stack occupies xi[0..head] while the output
|
||||
// grows down from xi[n), and head < top holds so they never overlap. Only in-triangle
|
||||
// stored entries (index > j for lower, index < j for upper) are reach successors; the
|
||||
// diagonal and any out-of-triangle entries of the stored matrix are skipped.
|
||||
// `innerNonZeroPtr` is the per-column nonzero count: pass it for an uncompressed matrix
|
||||
// so column j ends at outerIndexPtr[j]+innerNonZeroPtr[j]; pass nullptr (compressed) to
|
||||
// end at outerIndexPtr[j+1].
|
||||
template <bool Upper, typename StorageIndex>
|
||||
Index triangular_reach(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr,
|
||||
const StorageIndex* innerNonZeroPtr, const StorageIndex* bIdx, Index bCount, StorageIndex* xi,
|
||||
StorageIndex* pstack, uint8_t* mark, Index n) {
|
||||
Index top = n;
|
||||
for (Index r = 0; r < bCount; ++r) {
|
||||
StorageIndex root = bIdx[r];
|
||||
if (mark[root]) continue;
|
||||
|
||||
Index head = 0;
|
||||
xi[0] = root;
|
||||
while (head >= 0) {
|
||||
StorageIndex j = xi[head];
|
||||
Index colBeg = outerIndexPtr[j];
|
||||
Index colEnd = innerNonZeroPtr ? outerIndexPtr[j] + innerNonZeroPtr[j] : outerIndexPtr[j + 1];
|
||||
if (!mark[j]) {
|
||||
mark[j] = 1;
|
||||
pstack[head] = StorageIndex(colBeg);
|
||||
}
|
||||
bool done = true;
|
||||
for (Index p = pstack[head]; p < colEnd; ++p) {
|
||||
StorageIndex i = innerIndexPtr[p];
|
||||
if (Upper ? (i >= j) : (i <= j)) continue; // out of triangle, or the diagonal
|
||||
if (mark[i]) continue; // already visited
|
||||
pstack[head] = StorageIndex(p + 1);
|
||||
xi[++head] = i; // descend
|
||||
done = false;
|
||||
break;
|
||||
}
|
||||
if (done) { // no unvisited successor: postorder j
|
||||
xi[--top] = j;
|
||||
--head;
|
||||
}
|
||||
}
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Numeric sweep over a precomputed reach.
|
||||
// ===========================================================================
|
||||
|
||||
// Solves T x = b in place on the dense accumulator x (zero except where b was
|
||||
// scattered), touching only the reached columns in the order produced by the reach.
|
||||
// Columns are sorted, so the active triangle is a contiguous run: for lower it is the
|
||||
// suffix from the first entry with index >= j (the diagonal, then the sub-diagonal
|
||||
// off-diagonals); for upper it is the prefix up to the last entry with index <= j (the
|
||||
// off-diagonals, then the diagonal). Locating the boundary is O(1) for a genuinely
|
||||
// triangular column (the extreme stored entry is already the diagonal); only when a
|
||||
// stored column carries out-of-triangle entries (a general matrix seen through a
|
||||
// TriangularView) do we binary-search past them, in O(log nnz/col) rather than a
|
||||
// linear scan of the wrong-side run.
|
||||
// The stored lhs scalar (LhsScalar, read from valuePtr) and the accumulator/rhs scalar
|
||||
// (RhsScalar, held in x) are separate: a real factor applied to a complex rhs must
|
||||
// accumulate in the complex type. The arithmetic runs in RhsScalar; LhsScalar values
|
||||
// promote to it (e.g. double -> complex<double>).
|
||||
template <bool Upper, bool UnitDiag, typename StorageIndex, typename LhsScalar, typename RhsScalar>
|
||||
void triangular_solve_over_reach(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr,
|
||||
const LhsScalar* valuePtr, const StorageIndex* innerNonZeroPtr, const StorageIndex* xi,
|
||||
Index top, Index n, RhsScalar* x) {
|
||||
for (Index k = top; k < n; ++k) {
|
||||
StorageIndex j = xi[k];
|
||||
Index colBeg = outerIndexPtr[j];
|
||||
Index colEnd = innerNonZeroPtr ? outerIndexPtr[j] + innerNonZeroPtr[j] : outerIndexPtr[j + 1];
|
||||
RhsScalar xj;
|
||||
Index offBeg, offEnd;
|
||||
EIGEN_IF_CONSTEXPR (Upper) {
|
||||
// e = one past the last in-triangle entry (index <= j)
|
||||
Index e = colEnd;
|
||||
if (e > colBeg && innerIndexPtr[e - 1] > j) // wrong-side (below-diagonal) tail: skip it
|
||||
e = std::upper_bound(innerIndexPtr + colBeg, innerIndexPtr + colEnd, j) - innerIndexPtr;
|
||||
bool hasDiag = e > colBeg && innerIndexPtr[e - 1] == j;
|
||||
offBeg = colBeg;
|
||||
offEnd = hasDiag ? e - 1 : e; // drop the diagonal slot from the update iff it is stored
|
||||
EIGEN_IF_CONSTEXPR (!UnitDiag) {
|
||||
eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
|
||||
// Missing diagonal is out of contract; match the old AmbiVector path -- divide by 0
|
||||
// for a deterministic inf/NaN, rather than reading valuePtr[e-1] out of bounds.
|
||||
x[j] /= hasDiag ? valuePtr[e - 1] : LhsScalar(0);
|
||||
}
|
||||
} else {
|
||||
// s = first in-triangle entry (index >= j)
|
||||
Index s = colBeg;
|
||||
if (s < colEnd && innerIndexPtr[s] < j) // wrong-side (above-diagonal) head: skip it
|
||||
s = std::lower_bound(innerIndexPtr + colBeg, innerIndexPtr + colEnd, j) - innerIndexPtr;
|
||||
bool hasDiag = s < colEnd && innerIndexPtr[s] == j;
|
||||
offBeg = hasDiag ? s + 1 : s; // drop the diagonal slot from the update iff it is stored
|
||||
offEnd = colEnd;
|
||||
EIGEN_IF_CONSTEXPR (!UnitDiag) {
|
||||
eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
|
||||
x[j] /= hasDiag ? valuePtr[s] : LhsScalar(0); // missing diagonal -> inf/NaN, not an OOB read
|
||||
}
|
||||
}
|
||||
xj = x[j];
|
||||
for (Index p = offBeg; p < offEnd; ++p) {
|
||||
StorageIndex i = innerIndexPtr[p];
|
||||
x[i] = numext::madd<RhsScalar>(-xj, RhsScalar(valuePtr[p]), x[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Borrow-a-buffer solve core.
|
||||
//
|
||||
// The caller owns the scratch and passes it in, so a reused buffer makes repeated
|
||||
// solves allocation-free. All buffers are restored on exit, so one setup suffices for
|
||||
// many solves.
|
||||
// ===========================================================================
|
||||
|
||||
// Core: compute reach(pattern(b)) and run the numeric sweep, returning top -- WITHOUT
|
||||
// any cleanup. The rhs must ALREADY be scattered into xwork (xwork[bIdx[r]] = value) by
|
||||
// the caller; bIdx is the rhs pattern (the reach roots). Pulling the scatter out lets a
|
||||
// caller reading the rhs through an iterator scatter as it reads, dropping the separate
|
||||
// value array. On return, xi = iwork[top..n) holds the reached columns in topological
|
||||
// order, xwork holds their solution values, and mark is set on the reached set; the
|
||||
// caller consumes xwork/xi and clears them.
|
||||
// Solving T x = b for a column-major, sorted triangular T (lower or upper):
|
||||
// - iwork: >= 2n StorageIndex, carved into xi | pstack (each length n).
|
||||
// - mark: >= n bytes, all-zero.
|
||||
// - xwork: >= n Scalar, the dense accumulator, zero except b scattered on bIdx.
|
||||
// `innerNonZeroPtr` is nullptr for a compressed T, or the per-column nonzero count for
|
||||
// an uncompressed T (columns then end at outerIndexPtr[j]+innerNonZeroPtr[j]).
|
||||
template <bool Upper, bool UnitDiag, typename StorageIndex, typename LhsScalar, typename RhsScalar>
|
||||
Index reach_solve_dense(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr, const LhsScalar* valuePtr,
|
||||
const StorageIndex* innerNonZeroPtr, Index n, const StorageIndex* bIdx, Index bCount,
|
||||
StorageIndex* iwork, uint8_t* mark, RhsScalar* xwork) {
|
||||
Index top =
|
||||
triangular_reach<Upper>(outerIndexPtr, innerIndexPtr, innerNonZeroPtr, bIdx, bCount, iwork, iwork + n, mark, n);
|
||||
triangular_solve_over_reach<Upper, UnitDiag>(outerIndexPtr, innerIndexPtr, valuePtr, innerNonZeroPtr, iwork, top, n,
|
||||
xwork);
|
||||
return top;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Iterator-driven path: for a triangular, column-major sparse expression that does NOT
|
||||
// expose raw CSC storage (has_compressed_access is false). Columns are read through the
|
||||
// expression's evaluator InnerIterator. Because an InnerIterator can't cheaply hold DFS
|
||||
// resume state, the reach uses a mark-on-push worklist plus a final sort into
|
||||
// topological order (ascending index for lower, descending for upper -- both valid
|
||||
// topological orders for the respective solve); the log factor is empirically ~free.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Reach via a mark-on-push worklist that opens one column at a time through
|
||||
// InnerIterator. Only in-triangle successors (index > j for lower, index < j for upper)
|
||||
// are pushed. Scratch: xi (n, shared stack+output) and mark (n bytes, all-zero in,
|
||||
// reach flagged out). Returns top; xi[top..n) is sorted into the topological order for
|
||||
// the solve direction.
|
||||
template <bool Upper, typename Eval, typename StorageIndex>
|
||||
Index triangular_reach_iter(const Eval& mat, const StorageIndex* bIdx, Index bCount, StorageIndex* xi, uint8_t* mark,
|
||||
Index n) {
|
||||
Index top = n;
|
||||
Index sp = 0;
|
||||
for (Index r = 0; r < bCount; ++r) {
|
||||
StorageIndex root = bIdx[r];
|
||||
if (!mark[root]) {
|
||||
mark[root] = 1;
|
||||
xi[sp++] = root;
|
||||
}
|
||||
}
|
||||
while (sp > 0) {
|
||||
StorageIndex j = xi[--sp];
|
||||
xi[--top] = j; // collect
|
||||
for (typename Eval::InnerIterator it(mat, j); it; ++it) {
|
||||
StorageIndex i = StorageIndex(it.index());
|
||||
if (Upper ? (i >= j) : (i <= j)) continue; // out of triangle, or the diagonal
|
||||
if (!mark[i]) {
|
||||
mark[i] = 1;
|
||||
xi[sp++] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
// descending for upper, ascending for lower (comparator type picked at compile time)
|
||||
using Comp = std::conditional_t<Upper, std::greater<StorageIndex>, std::less<StorageIndex>>;
|
||||
std::sort(xi + top, xi + n, Comp{});
|
||||
return top;
|
||||
}
|
||||
|
||||
// Numeric sweep over the reach, reading columns through InnerIterator. Direction-aware
|
||||
// like the pointer sweep: for lower the leading out-of-triangle entries (index < j) are
|
||||
// skipped so the diagonal is the first remaining entry; for upper the diagonal is found
|
||||
// by scan and only the entries with index < j are updated.
|
||||
template <bool Upper, bool UnitDiag, typename Eval, typename StorageIndex, typename Scalar>
|
||||
void triangular_solve_over_reach_iter(const Eval& mat, const StorageIndex* xi, Index top, Index n, Scalar* x) {
|
||||
for (Index k = top; k < n; ++k) {
|
||||
StorageIndex j = xi[k];
|
||||
EIGEN_IF_CONSTEXPR (Upper) {
|
||||
EIGEN_IF_CONSTEXPR (!UnitDiag) {
|
||||
Scalar d(0); // stays 0 if the diagonal is missing, so singularity surfaces as inf/NaN
|
||||
bool hasDiag = false;
|
||||
for (typename Eval::InnerIterator dt(mat, j); dt; ++dt)
|
||||
if (StorageIndex(dt.index()) == j) {
|
||||
d = dt.value();
|
||||
hasDiag = true;
|
||||
}
|
||||
eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
|
||||
x[j] /= d; // d == 0 when the diagonal is missing -> inf/NaN, consistent with the pointer path
|
||||
}
|
||||
Scalar xj = x[j];
|
||||
for (typename Eval::InnerIterator it(mat, j); it && StorageIndex(it.index()) < j; ++it)
|
||||
x[it.index()] = numext::madd<Scalar>(-xj, it.value(), x[it.index()]);
|
||||
} else {
|
||||
typename Eval::InnerIterator it(mat, j);
|
||||
while (it && StorageIndex(it.index()) < j) ++it; // skip out-of-triangle (index < j)
|
||||
bool hasDiag = it && StorageIndex(it.index()) == j; // diagonal is the first in-triangle entry
|
||||
EIGEN_IF_CONSTEXPR (!UnitDiag) {
|
||||
eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
|
||||
// Missing diagonal -> inf/NaN, not it.value() on an ended/wrong iterator (see pointer path).
|
||||
x[j] /= hasDiag ? it.value() : Scalar(0);
|
||||
}
|
||||
if (hasDiag) ++it; // step past the stored diagonal (the divisor above, or a unit entry)
|
||||
Scalar xj = x[j];
|
||||
for (; it; ++it) x[it.index()] = numext::madd<Scalar>(-xj, it.value(), x[it.index()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterator core: reach + numeric, returning top (no cleanup), the iterator counterpart
|
||||
// of reach_solve_dense -- xwork must already hold the scattered rhs. Uses the same 2n /
|
||||
// n(bytes) / n workspace layout (the pstack half of iwork is left unused), so the two
|
||||
// general paths share one workspace contract.
|
||||
template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
|
||||
Index reach_solve_dense_iter(const LhsType& lhs, Index n, const StorageIndex* bIdx, Index bCount, StorageIndex* iwork,
|
||||
uint8_t* mark, Scalar* xwork) {
|
||||
evaluator<LhsType> mat(lhs);
|
||||
Index top = triangular_reach_iter<Upper>(mat, bIdx, bCount, iwork, mark, n);
|
||||
triangular_solve_over_reach_iter<Upper, UnitDiag>(mat, iwork, top, n, xwork);
|
||||
return top;
|
||||
}
|
||||
|
||||
// Policy dispatch for the core (returns top): an expression that exposes raw storage
|
||||
// takes the pointer + DFS fast path; anything else takes the evaluator + worklist path.
|
||||
// Tag dispatch (not if-constexpr) keeps the untaken branch from being instantiated, so
|
||||
// outerIndexPtr() is never named on a type that lacks it.
|
||||
//
|
||||
// CompressedAccessBit is a compile-time capability, not a guarantee the instance is
|
||||
// compressed: an uncompressed SparseMatrix keeps per-column gaps addressed via
|
||||
// innerNonZeroPtr(), so its columns do NOT run to outerIndexPtr()[j+1]. Passing
|
||||
// innerNonZeroPtr() through keeps the raw-pointer path valid either way -- it is nullptr
|
||||
// exactly when compressed (columns end at outerIndexPtr[j+1]) and the per-column count
|
||||
// otherwise (columns end at outerIndexPtr[j]+innerNonZeroPtr[j]).
|
||||
template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
|
||||
Index reach_solve_dense_dispatch(std::true_type /*compressed*/, const LhsType& lhs, Index n, const StorageIndex* bIdx,
|
||||
Index bCount, StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
|
||||
return reach_solve_dense<Upper, UnitDiag>(lhs.outerIndexPtr(), lhs.innerIndexPtr(), lhs.valuePtr(),
|
||||
lhs.innerNonZeroPtr(), n, bIdx, bCount, iwork, mark, xwork);
|
||||
}
|
||||
template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
|
||||
Index reach_solve_dense_dispatch(std::false_type /*iterator*/, const LhsType& lhs, Index n, const StorageIndex* bIdx,
|
||||
Index bCount, StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
|
||||
return reach_solve_dense_iter<Upper, UnitDiag>(lhs, n, bIdx, bCount, iwork, mark, xwork);
|
||||
}
|
||||
|
||||
// Expression core: solve T x = b for a sparse-expression triangular T with the rhs
|
||||
// PRE-SCATTERED into xwork (bIdx is its pattern), selecting the pointer or iterator path
|
||||
// at compile time; RETURNS top with the solution left in xwork and the reach in
|
||||
// iwork[top..n) (see reach_solve_dense). This is what the sparse selector uses -- it
|
||||
// scatters the rhs as it reads it and consumes xwork directly, so no bVal/outIdx/outVal.
|
||||
template <bool Upper, bool UnitDiag, typename LhsDerived, typename StorageIndex, typename Scalar>
|
||||
Index reach_solve_dense(const SparseMatrixBase<LhsDerived>& lhs, const StorageIndex* bIdx, Index bCount,
|
||||
StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
|
||||
return reach_solve_dense_dispatch<Upper, UnitDiag>(
|
||||
std::integral_constant<bool, has_compressed_access<LhsDerived>::value>{}, lhs.derived(), lhs.rows(), bIdx, bCount,
|
||||
iwork, mark, xwork);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace Eigen
|
||||
|
||||
#endif // EIGEN_TRIANGULAR_REACH_SOLVER_H
|
||||
@@ -182,64 +182,126 @@ template <typename Lhs, typename Rhs, int Mode,
|
||||
int StorageOrder = int(Lhs::Flags) & RowMajorBit>
|
||||
struct sparse_solve_triangular_sparse_selector;
|
||||
|
||||
// True when the rhs exposes raw CSC storage with a StorageIndex matching the lhs, so a
|
||||
// column's stored index slice can serve as the reach roots directly (no bIdx copy). A
|
||||
// SparseVector qualifies too -- it is a single compressed column, handled below via the
|
||||
// null-outerIndexPtr guard (its outerIndexPtr() is null since it has no outer array).
|
||||
template <typename Lhs, typename Rhs>
|
||||
using rhs_matching_slice = std::integral_constant<
|
||||
bool, has_compressed_access<Rhs>::value &&
|
||||
std::is_same<typename traits<Rhs>::StorageIndex, typename traits<Lhs>::StorageIndex>::value>;
|
||||
|
||||
// The reach for a column arrives in one of three compile-time-known orders, but the
|
||||
// output column must store ascending inner index. reach_reorder encapsulates the
|
||||
// per-order fix-up via partial specialization: an unordered reach (Ordered == false,
|
||||
// the pointer/DFS path -- Upper irrelevant) is sorted; the iterator reach is already
|
||||
// in solve order -- ascending for lower (no-op), descending for upper (reverse).
|
||||
template <bool Ordered, bool Upper, typename StorageIndex>
|
||||
struct reach_reorder { // Ordered == false: unordered pointer/DFS reach
|
||||
static void run(StorageIndex* first, StorageIndex* last) { std::sort(first, last); }
|
||||
};
|
||||
template <typename StorageIndex>
|
||||
struct reach_reorder<true, false, StorageIndex> { // iterator reach, lower: already ascending
|
||||
static void run(StorageIndex* /*first*/, StorageIndex* /*last*/) {}
|
||||
};
|
||||
template <typename StorageIndex>
|
||||
struct reach_reorder<true, true, StorageIndex> { // iterator reach, upper: descending
|
||||
static void run(StorageIndex* first, StorageIndex* last) { std::reverse(first, last); }
|
||||
};
|
||||
|
||||
// Common per-column finish: reorder the reach xi[top..n) to ascending inner index,
|
||||
// insert reading values from xwork, and clear xwork and mark for the next column.
|
||||
// The reach is a structural bound, not a numeric one: a reached coefficient can be
|
||||
// exactly zero (a zero rhs entry, or numerical cancellation), so skip exact zeros at
|
||||
// insertion. This matches the AmbiVector path, which pruned zeros, and keeps a zero rhs
|
||||
// from materializing O(|reach|) stored zeros. xwork and mark are cleared regardless.
|
||||
template <bool Ordered, bool Upper, typename Res, typename StorageIndex, typename Scalar>
|
||||
void reach_insert_column(Res& res, Index col, StorageIndex* xi, Index top, Index n, Scalar* xwork, uint8_t* mark) {
|
||||
reach_reorder<Ordered, Upper, StorageIndex>::run(xi + top, xi + n);
|
||||
for (Index k = top; k < n; ++k) {
|
||||
StorageIndex j = xi[k];
|
||||
if (!numext::is_exactly_zero(xwork[j])) res.insert(j, col) = xwork[j];
|
||||
xwork[j] = Scalar(0);
|
||||
mark[j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Column loop, fast path: the rhs matches (see rhs_matching_slice), so each column's
|
||||
// stored index slice is the reach root list and the value slice is scattered directly
|
||||
// -- no bIdx copy, so iwork is just 2n (xi | pstack).
|
||||
template <bool Upper, bool UnitDiag, typename Lhs, typename Rhs, typename Res, typename Scalar,
|
||||
std::enable_if_t<rhs_matching_slice<Lhs, Rhs>::value, int> = 0>
|
||||
void reach_solve_columns(const Lhs& lhs, const Rhs& other, Res& res, uint8_t* mark, Scalar* xwork, Index n) {
|
||||
typedef typename traits<Lhs>::StorageIndex StorageIndex;
|
||||
Matrix<StorageIndex, Dynamic, 1> iwork(2 * n); // xi | pstack
|
||||
StorageIndex* xi = iwork.data();
|
||||
for (Index col = 0; col < other.cols(); ++col) {
|
||||
const StorageIndex* outer = other.outerIndexPtr(); // null for a SparseVector (single column)
|
||||
const StorageIndex* nnz = other.innerNonZeroPtr(); // null when compressed
|
||||
Index p = outer ? outer[col] : 0;
|
||||
Index bCount = outer ? (nnz ? Index(nnz[col]) : Index(outer[col + 1]) - p) : other.nonZeros();
|
||||
const StorageIndex* roots = other.innerIndexPtr() + p; // the column's stored indices
|
||||
const Scalar* vals = other.valuePtr() + p;
|
||||
// Roots are the rhs slice directly (no bIdx copy, so iwork stays 2n). An exact-zero
|
||||
// stored rhs entry is seeded harmlessly -- it propagates zeros and is dropped at
|
||||
// insertion; filtering it here would cost a compacted root buffer (the 3n path).
|
||||
for (Index r = 0; r < bCount; ++r) xwork[roots[r]] = vals[r];
|
||||
Index top = reach_solve_dense<Upper, UnitDiag>(lhs, roots, bCount, xi, mark, xwork);
|
||||
reach_insert_column<!has_compressed_access<Lhs>::value, Upper>(res, col, xi, top, n, xwork, mark);
|
||||
}
|
||||
}
|
||||
|
||||
// Column loop, fallback: read each column through the InnerIterator, copying indices
|
||||
// into the bIdx third of a 3n iwork. For a rhs without raw storage or with a
|
||||
// mismatched index type.
|
||||
template <bool Upper, bool UnitDiag, typename Lhs, typename Rhs, typename Res, typename Scalar,
|
||||
std::enable_if_t<!rhs_matching_slice<Lhs, Rhs>::value, int> = 0>
|
||||
void reach_solve_columns(const Lhs& lhs, const Rhs& other, Res& res, uint8_t* mark, Scalar* xwork, Index n) {
|
||||
typedef typename traits<Lhs>::StorageIndex StorageIndex;
|
||||
Matrix<StorageIndex, Dynamic, 1> iwork(3 * n); // xi | pstack | bIdx
|
||||
StorageIndex* xi = iwork.data();
|
||||
StorageIndex* bIdx = iwork.data() + 2 * n;
|
||||
for (Index col = 0; col < other.cols(); ++col) {
|
||||
Index bCount = 0;
|
||||
for (typename Rhs::InnerIterator it(other, col); it; ++it) {
|
||||
if (numext::is_exactly_zero(it.value())) continue; // a zero root seeds nothing; xwork stays clear there
|
||||
bIdx[bCount] = StorageIndex(it.index());
|
||||
xwork[it.index()] = it.value();
|
||||
++bCount;
|
||||
}
|
||||
Index top = reach_solve_dense<Upper, UnitDiag>(lhs, bIdx, bCount, xi, mark, xwork);
|
||||
reach_insert_column<!has_compressed_access<Lhs>::value, Upper>(res, col, xi, top, n, xwork, mark);
|
||||
}
|
||||
}
|
||||
|
||||
// Reach-based (Gilbert-Peierls) sparse triangular solve, col-major, for lower OR
|
||||
// upper. Only the columns reachable from each rhs column's pattern are touched, so
|
||||
// the cost is O(|reach| + flops) per column instead of a dense O(n)-per-column sweep
|
||||
// (which also pays a coeff(i,i) binary search per row in the upper case). It is
|
||||
// the sole col-major sparse-sparse selector, dispatching lower/upper via the UpLo
|
||||
// template argument. reach_solve_dense leaves the solution values in
|
||||
// xwork and the reached indices in iwork[top..n); reach_solve_columns (slice or
|
||||
// fallback, selected on the rhs storage) scatters each column and solves, and
|
||||
// reach_insert_column reads the values out and restores mark/xwork. Only mark and
|
||||
// xwork need zeroing -- iwork is entirely written before read.
|
||||
template <bool Upper, typename Lhs, typename Rhs, int Mode>
|
||||
void run_sparse_reach_triangular_solve(const Lhs& lhs, Rhs& other) {
|
||||
typedef typename Rhs::Scalar Scalar;
|
||||
Index n = lhs.rows();
|
||||
Matrix<uint8_t, Dynamic, 1> mark = Matrix<uint8_t, Dynamic, 1>::Zero(n);
|
||||
Matrix<Scalar, Dynamic, 1> xwork = Matrix<Scalar, Dynamic, 1>::Zero(n);
|
||||
Rhs res(other.rows(), other.cols());
|
||||
res.reserve(other.nonZeros());
|
||||
reach_solve_columns<Upper, bool(Mode & UnitDiag)>(lhs, other, res, mark.data(), xwork.data(), n);
|
||||
res.finalize();
|
||||
other = res.markAsRValue();
|
||||
}
|
||||
|
||||
// forward and backward substitution, col-major
|
||||
template <typename Lhs, typename Rhs, int Mode, int UpLo>
|
||||
struct sparse_solve_triangular_sparse_selector<Lhs, Rhs, Mode, UpLo, ColMajor> {
|
||||
typedef typename Rhs::Scalar Scalar;
|
||||
typedef typename promote_index_type<typename traits<Lhs>::StorageIndex, typename traits<Rhs>::StorageIndex>::type
|
||||
StorageIndex;
|
||||
static void run(const Lhs& lhs, Rhs& other) {
|
||||
const bool IsLower = (UpLo == Lower);
|
||||
AmbiVector<Scalar, StorageIndex> tempVector(other.rows() * 2);
|
||||
tempVector.setBounds(0, other.rows());
|
||||
|
||||
Rhs res(other.rows(), other.cols());
|
||||
res.reserve(other.nonZeros());
|
||||
|
||||
for (Index col = 0; col < other.cols(); ++col) {
|
||||
// FIXME: estimate the number of non-zeros per column for better allocation.
|
||||
tempVector.init(.99 /*float(other.col(col).nonZeros())/float(other.rows())*/);
|
||||
tempVector.setZero();
|
||||
tempVector.restart();
|
||||
for (typename Rhs::InnerIterator rhsIt(other, col); rhsIt; ++rhsIt) {
|
||||
tempVector.coeffRef(rhsIt.index()) = rhsIt.value();
|
||||
}
|
||||
|
||||
for (Index i = IsLower ? 0 : lhs.cols() - 1; IsLower ? i < lhs.cols() : i >= 0; i += IsLower ? 1 : -1) {
|
||||
tempVector.restart();
|
||||
Scalar& ci = tempVector.coeffRef(i);
|
||||
if (!numext::is_exactly_zero(ci)) {
|
||||
// find
|
||||
typename Lhs::InnerIterator it(lhs, i);
|
||||
EIGEN_IF_CONSTEXPR (!(Mode & UnitDiag)) {
|
||||
EIGEN_IF_CONSTEXPR (IsLower) {
|
||||
eigen_assert(it.index() == i);
|
||||
ci /= it.value();
|
||||
} else
|
||||
ci /= lhs.coeff(i, i);
|
||||
}
|
||||
tempVector.restart();
|
||||
EIGEN_IF_CONSTEXPR (IsLower) {
|
||||
if (it.index() == i) ++it;
|
||||
for (; it; ++it) {
|
||||
tempVector.coeffRef(it.index()) = numext::madd<Scalar>(-ci, it.value(), tempVector.coeffRef(it.index()));
|
||||
}
|
||||
} else {
|
||||
for (; it && it.index() < i; ++it) {
|
||||
tempVector.coeffRef(it.index()) = numext::madd<Scalar>(-ci, it.value(), tempVector.coeffRef(it.index()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: compute a reference value to filter zeros.
|
||||
for (typename AmbiVector<Scalar, StorageIndex>::Iterator it(tempVector /*,1e-12*/); it; ++it) {
|
||||
// FIXME: use insertBack for better performance.
|
||||
res.insert(it.index(), col) = it.value();
|
||||
}
|
||||
}
|
||||
res.finalize();
|
||||
other = res.markAsRValue();
|
||||
run_sparse_reach_triangular_solve<UpLo == Upper, Lhs, Rhs, Mode>(lhs, other);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,144 @@ void sparse_solvers(int rows, int cols) {
|
||||
m2.template triangularView<Upper>().solveInPlace(matB);
|
||||
VERIFY_IS_APPROX(matB, refMatB);
|
||||
|
||||
// A triangularView is a view of the triangular PART of a possibly-general matrix,
|
||||
// so the stored matrix need not be strictly triangular. Exercise a general lhs, a
|
||||
// SparseVector rhs, a mismatched-StorageIndex rhs, an uncompressed lhs, an
|
||||
// expression lhs, and a unit diagonal -- none of which the checks above cover.
|
||||
{
|
||||
SparseMatrix<Scalar> mg(rows, rows);
|
||||
DenseMatrix refMatG = DenseMatrix::Zero(rows, rows);
|
||||
initSparse<Scalar>(density, refMatG, mg, ForceNonZeroDiag); // GENERAL (both triangles stored)
|
||||
initSparse<Scalar>(density, refMatB, matB);
|
||||
|
||||
// general matrix through a lower / upper / unit-upper view, sparse rhs
|
||||
for (int mode = 0; mode < 3; ++mode) {
|
||||
DenseMatrix rb = refMatB;
|
||||
SparseMatrix<Scalar> mb = matB;
|
||||
if (mode == 0) {
|
||||
refMatG.template triangularView<Lower>().solveInPlace(rb);
|
||||
mg.template triangularView<Lower>().solveInPlace(mb);
|
||||
} else if (mode == 1) {
|
||||
refMatG.template triangularView<Upper>().solveInPlace(rb);
|
||||
mg.template triangularView<Upper>().solveInPlace(mb);
|
||||
} else {
|
||||
refMatG.template triangularView<UnitUpper>().solveInPlace(rb);
|
||||
mg.template triangularView<UnitUpper>().solveInPlace(mb);
|
||||
}
|
||||
VERIFY_IS_APPROX(mb.toDense(), rb);
|
||||
}
|
||||
|
||||
// expression lhs (no raw storage -> iterator path)
|
||||
{
|
||||
DenseMatrix rb = refMatB;
|
||||
SparseMatrix<Scalar> mb = matB;
|
||||
refMatG.template triangularView<Upper>().solveInPlace(rb);
|
||||
(Scalar(1) * mg).template triangularView<Upper>().solveInPlace(mb);
|
||||
VERIFY_IS_APPROX(mb.toDense(), rb);
|
||||
}
|
||||
|
||||
// uncompressed lhs (innerNonZeroPtr != null)
|
||||
{
|
||||
DenseMatrix rb = refMatB;
|
||||
SparseMatrix<Scalar> mb = matB, mu = mg;
|
||||
mu.reserve(Matrix<int, Dynamic, 1>::Constant(mu.cols(), rows)); // -> uncompressed
|
||||
refMatG.template triangularView<Lower>().solveInPlace(rb);
|
||||
mu.template triangularView<Lower>().solveInPlace(mb);
|
||||
VERIFY_IS_APPROX(mb.toDense(), rb);
|
||||
}
|
||||
|
||||
// mismatched-StorageIndex rhs (-> InnerIterator fallback)
|
||||
{
|
||||
DenseMatrix rb = refMatB;
|
||||
SparseMatrix<Scalar, ColMajor, long> mbl = matB;
|
||||
refMatG.template triangularView<Lower>().solveInPlace(rb);
|
||||
mg.template triangularView<Lower>().solveInPlace(mbl);
|
||||
VERIFY_IS_APPROX(DenseMatrix(mbl), rb);
|
||||
}
|
||||
|
||||
// SparseVector rhs (sets CompressedAccessBit but its outerIndexPtr() is null)
|
||||
{
|
||||
DenseVector rv = DenseVector::Zero(rows);
|
||||
SparseVector<Scalar> vb(rows);
|
||||
for (Index i = 0; i < rows; ++i)
|
||||
if (internal::random<int>(0, 2) == 0) {
|
||||
Scalar s = internal::random<Scalar>();
|
||||
vb.coeffRef(i) = s;
|
||||
rv(i) = s;
|
||||
}
|
||||
DenseVector rref = refMatG.template triangularView<Lower>().solve(rv);
|
||||
SparseVector<Scalar> vx = vb;
|
||||
mg.template triangularView<Lower>().solveInPlace(vx);
|
||||
VERIFY_IS_APPROX(DenseVector(vx), rref);
|
||||
}
|
||||
|
||||
// explicitly-stored zero rhs entries must not expand into stored zeros: the reach
|
||||
// is a structural bound, so a zero rhs coefficient (or one that cancels to zero)
|
||||
// is pruned at insertion rather than materialized across the whole reach.
|
||||
{
|
||||
SparseMatrix<Scalar> mb(rows, matB.cols());
|
||||
DenseMatrix rb = DenseMatrix::Zero(rows, matB.cols());
|
||||
for (Index c = 0; c < mb.cols(); ++c)
|
||||
for (Index i = 0; i < rows; ++i)
|
||||
if (internal::random<int>(0, 3) == 0) {
|
||||
Scalar s = internal::random<int>(0, 2) == 0 ? Scalar(0) : internal::random<Scalar>(); // some explicit 0
|
||||
mb.insert(i, c) = s;
|
||||
rb(i, c) = s;
|
||||
}
|
||||
mb.makeCompressed();
|
||||
refMatG.template triangularView<Lower>().solveInPlace(rb);
|
||||
mg.template triangularView<Lower>().solveInPlace(mb);
|
||||
VERIFY_IS_APPROX(mb.toDense(), rb);
|
||||
for (Index c = 0; c < mb.cols(); ++c)
|
||||
for (typename SparseMatrix<Scalar>::InnerIterator it(mb, c); it; ++it)
|
||||
VERIFY(!numext::is_exactly_zero(it.value())); // no stored zeros
|
||||
}
|
||||
|
||||
// A reached column with no stored diagonal (non-unit) is out of contract: it must
|
||||
// assert in debug on every path (pointer/iterator x lower/upper), rather than the
|
||||
// failure being silently keyed to has_compressed_access. In release these divide by
|
||||
// zero -> inf/NaN with no out-of-bounds read (covered by the sanitizer drivers).
|
||||
{
|
||||
SparseMatrix<Scalar> us(3, 3);
|
||||
us.insert(0, 1) = Scalar(1);
|
||||
us.insert(1, 1) = Scalar(2);
|
||||
us.insert(2, 2) = Scalar(3);
|
||||
us.makeCompressed(); // column 0 empty -> reached from rhs(0) but no diagonal
|
||||
SparseMatrix<Scalar> ub(3, 1);
|
||||
ub.insert(0, 0) = Scalar(1);
|
||||
ub.makeCompressed();
|
||||
SparseMatrix<Scalar> up = ub, ui = ub;
|
||||
VERIFY_RAISES_ASSERT(us.template triangularView<Upper>().solveInPlace(up)); // pointer upper
|
||||
VERIFY_RAISES_ASSERT((Scalar(1) * us).template triangularView<Upper>().solveInPlace(ui)); // iterator upper
|
||||
|
||||
SparseMatrix<Scalar> ls(3, 3);
|
||||
ls.insert(0, 0) = Scalar(2);
|
||||
ls.insert(1, 1) = Scalar(3);
|
||||
ls.makeCompressed(); // column 2 empty (last) -> reached from rhs(2) but no diagonal
|
||||
SparseMatrix<Scalar> lb(3, 1);
|
||||
lb.insert(2, 0) = Scalar(1);
|
||||
lb.makeCompressed();
|
||||
SparseMatrix<Scalar> lp = lb, li = lb;
|
||||
VERIFY_RAISES_ASSERT(ls.template triangularView<Lower>().solveInPlace(lp)); // pointer lower
|
||||
VERIFY_RAISES_ASSERT((Scalar(1) * ls).template triangularView<Lower>().solveInPlace(li)); // iterator lower
|
||||
}
|
||||
|
||||
// mixed-scalar rhs: a real lhs applied to a rhs must accumulate in the rhs scalar.
|
||||
// For real Scalar this is the ordinary path; for complex Scalar it is the
|
||||
// real-factor / complex-data case that must both compile and be correct.
|
||||
{
|
||||
typedef typename NumTraits<Scalar>::Real Real;
|
||||
SparseMatrix<Real> mr(rows, rows);
|
||||
Matrix<Real, Dynamic, Dynamic> refMatR = Matrix<Real, Dynamic, Dynamic>::Zero(rows, rows);
|
||||
initSparse<Real>(density, refMatR, mr, ForceNonZeroDiag);
|
||||
DenseMatrix rb = refMatB;
|
||||
SparseMatrix<Scalar> mb = matB;
|
||||
refMatR.template cast<Scalar>().template triangularView<Lower>().solveInPlace(rb);
|
||||
mr.template triangularView<Lower>().solveInPlace(mb); // SparseMatrix<Real> lhs, <Scalar> rhs
|
||||
VERIFY_IS_APPROX(mb.toDense(), rb);
|
||||
}
|
||||
}
|
||||
|
||||
// test deprecated API
|
||||
initSparse<Scalar>(density, refMat2, m2, ForceNonZeroDiag | MakeLowerTriangular, &zeroCoords, &nonzeroCoords);
|
||||
VERIFY_IS_APPROX(refMat2.template triangularView<Lower>().solve(vec2),
|
||||
|
||||
Reference in New Issue
Block a user