blob: ec265c86f21c91a42b003c1fdb58fc99d81d5a5a [file]
// 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>(bool_constant<has_compressed_access<LhsDerived>::value>{},
lhs.derived(), lhs.rows(), bIdx, bCount, iwork, mark, xwork);
}
} // namespace internal
} // namespace Eigen
#endif // EIGEN_TRIANGULAR_REACH_SOLVER_H