Eigen  5.0.1
 
Loading...
Searching...
No Matches
TriangularReachSolver.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// This Source Code Form is subject to the terms of the Mozilla
5// Public License v. 2.0. If a copy of the MPL was not distributed
6// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
7// SPDX-FileCopyrightText: The Eigen Authors
8// SPDX-License-Identifier: MPL-2.0
9
10#ifndef EIGEN_TRIANGULAR_REACH_SOLVER_H
11#define EIGEN_TRIANGULAR_REACH_SOLVER_H
12
13// IWYU pragma: private
14#include "./InternalHeaderCheck.h"
15
16namespace Eigen {
17
18namespace internal {
19
20// Gilbert-Peierls sparse-rhs triangular solve.
21//
22// Solves T x = b for a column-major sparse triangular T (lower OR upper) with sorted
23// columns, and a sparse right-hand side b. The cost is O(|reach| + flops): only the
24// columns reachable from b's pattern are touched, independent of the dimension.
25//
26// T is the triangular VIEW of a possibly-general stored matrix, so a column may hold
27// entries on BOTH sides of the diagonal; only the entries in the active triangle
28// participate. Everything is therefore direction-aware via a compile-time bool `Upper`:
29// - the reach follows only in-triangle successors (strictly below the diagonal for
30// lower, strictly above for upper -- one comparison per stored entry);
31// - the numeric sweep locates the diagonal within the sorted column (first entry with
32// index >= j for lower, last with index <= j for upper) and updates only the
33// in-triangle off-diagonal entries.
34// For a genuinely triangular stored matrix (Eigen's SparseMatrix) the in-triangle
35// entries are the whole column and the diagonal sits at the extreme, so the boundary
36// scans are single-comparison no-ops.
37
38// ===========================================================================
39// Reach: any triangular view, raw CSC storage.
40// ===========================================================================
41
42// Computes reach_{G(T)}(pattern(b)) via a non-recursive depth-first search, emitting
43// the reached columns into xi[top..n) in topological (solve) order and returning top.
44// `mark` (length-n byte array; a 0/1 visited flag needs no more) must be all-zero on
45// entry; every reached node is flagged, and since the reached set is exactly the
46// output, the caller clears those flags again while gathering (no reset needed). `xi`
47// and `pstack` are size-n scratch; the DFS stack occupies xi[0..head] while the output
48// grows down from xi[n), and head < top holds so they never overlap. Only in-triangle
49// stored entries (index > j for lower, index < j for upper) are reach successors; the
50// diagonal and any out-of-triangle entries of the stored matrix are skipped.
51// `innerNonZeroPtr` is the per-column nonzero count: pass it for an uncompressed matrix
52// so column j ends at outerIndexPtr[j]+innerNonZeroPtr[j]; pass nullptr (compressed) to
53// end at outerIndexPtr[j+1].
54template <bool Upper, typename StorageIndex>
55Index triangular_reach(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr,
56 const StorageIndex* innerNonZeroPtr, const StorageIndex* bIdx, Index bCount, StorageIndex* xi,
57 StorageIndex* pstack, uint8_t* mark, Index n) {
58 Index top = n;
59 for (Index r = 0; r < bCount; ++r) {
60 StorageIndex root = bIdx[r];
61 if (mark[root]) continue;
62
63 Index head = 0;
64 xi[0] = root;
65 while (head >= 0) {
66 StorageIndex j = xi[head];
67 Index colBeg = outerIndexPtr[j];
68 Index colEnd = innerNonZeroPtr ? outerIndexPtr[j] + innerNonZeroPtr[j] : outerIndexPtr[j + 1];
69 if (!mark[j]) {
70 mark[j] = 1;
71 pstack[head] = StorageIndex(colBeg);
72 }
73 bool done = true;
74 for (Index p = pstack[head]; p < colEnd; ++p) {
75 StorageIndex i = innerIndexPtr[p];
76 if (Upper ? (i >= j) : (i <= j)) continue; // out of triangle, or the diagonal
77 if (mark[i]) continue; // already visited
78 pstack[head] = StorageIndex(p + 1);
79 xi[++head] = i; // descend
80 done = false;
81 break;
82 }
83 if (done) { // no unvisited successor: postorder j
84 xi[--top] = j;
85 --head;
86 }
87 }
88 }
89 return top;
90}
91
92// ===========================================================================
93// Numeric sweep over a precomputed reach.
94// ===========================================================================
95
96// Solves T x = b in place on the dense accumulator x (zero except where b was
97// scattered), touching only the reached columns in the order produced by the reach.
98// Columns are sorted, so the active triangle is a contiguous run: for lower it is the
99// suffix from the first entry with index >= j (the diagonal, then the sub-diagonal
100// off-diagonals); for upper it is the prefix up to the last entry with index <= j (the
101// off-diagonals, then the diagonal). Locating the boundary is O(1) for a genuinely
102// triangular column (the extreme stored entry is already the diagonal); only when a
103// stored column carries out-of-triangle entries (a general matrix seen through a
104// TriangularView) do we binary-search past them, in O(log nnz/col) rather than a
105// linear scan of the wrong-side run.
106// The stored lhs scalar (LhsScalar, read from valuePtr) and the accumulator/rhs scalar
107// (RhsScalar, held in x) are separate: a real factor applied to a complex rhs must
108// accumulate in the complex type. The arithmetic runs in RhsScalar; LhsScalar values
109// promote to it (e.g. double -> complex<double>).
110template <bool Upper, bool UnitDiag, typename StorageIndex, typename LhsScalar, typename RhsScalar>
111void triangular_solve_over_reach(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr,
112 const LhsScalar* valuePtr, const StorageIndex* innerNonZeroPtr, const StorageIndex* xi,
113 Index top, Index n, RhsScalar* x) {
114 for (Index k = top; k < n; ++k) {
115 StorageIndex j = xi[k];
116 Index colBeg = outerIndexPtr[j];
117 Index colEnd = innerNonZeroPtr ? outerIndexPtr[j] + innerNonZeroPtr[j] : outerIndexPtr[j + 1];
118 RhsScalar xj;
119 Index offBeg, offEnd;
120 EIGEN_IF_CONSTEXPR (Upper) {
121 // e = one past the last in-triangle entry (index <= j)
122 Index e = colEnd;
123 if (e > colBeg && innerIndexPtr[e - 1] > j) // wrong-side (below-diagonal) tail: skip it
124 e = std::upper_bound(innerIndexPtr + colBeg, innerIndexPtr + colEnd, j) - innerIndexPtr;
125 bool hasDiag = e > colBeg && innerIndexPtr[e - 1] == j;
126 offBeg = colBeg;
127 offEnd = hasDiag ? e - 1 : e; // drop the diagonal slot from the update iff it is stored
128 EIGEN_IF_CONSTEXPR (!UnitDiag) {
129 eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
130 // Missing diagonal is out of contract; match the old AmbiVector path -- divide by 0
131 // for a deterministic inf/NaN, rather than reading valuePtr[e-1] out of bounds.
132 x[j] /= hasDiag ? valuePtr[e - 1] : LhsScalar(0);
133 }
134 } else {
135 // s = first in-triangle entry (index >= j)
136 Index s = colBeg;
137 if (s < colEnd && innerIndexPtr[s] < j) // wrong-side (above-diagonal) head: skip it
138 s = std::lower_bound(innerIndexPtr + colBeg, innerIndexPtr + colEnd, j) - innerIndexPtr;
139 bool hasDiag = s < colEnd && innerIndexPtr[s] == j;
140 offBeg = hasDiag ? s + 1 : s; // drop the diagonal slot from the update iff it is stored
141 offEnd = colEnd;
142 EIGEN_IF_CONSTEXPR (!UnitDiag) {
143 eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
144 x[j] /= hasDiag ? valuePtr[s] : LhsScalar(0); // missing diagonal -> inf/NaN, not an OOB read
145 }
146 }
147 xj = x[j];
148 for (Index p = offBeg; p < offEnd; ++p) {
149 StorageIndex i = innerIndexPtr[p];
150 x[i] = numext::madd<RhsScalar>(-xj, RhsScalar(valuePtr[p]), x[i]);
151 }
152 }
153}
154
155// ===========================================================================
156// Borrow-a-buffer solve core.
157//
158// The caller owns the scratch and passes it in, so a reused buffer makes repeated
159// solves allocation-free. All buffers are restored on exit, so one setup suffices for
160// many solves.
161// ===========================================================================
162
163// Core: compute reach(pattern(b)) and run the numeric sweep, returning top -- WITHOUT
164// any cleanup. The rhs must ALREADY be scattered into xwork (xwork[bIdx[r]] = value) by
165// the caller; bIdx is the rhs pattern (the reach roots). Pulling the scatter out lets a
166// caller reading the rhs through an iterator scatter as it reads, dropping the separate
167// value array. On return, xi = iwork[top..n) holds the reached columns in topological
168// order, xwork holds their solution values, and mark is set on the reached set; the
169// caller consumes xwork/xi and clears them.
170// Solving T x = b for a column-major, sorted triangular T (lower or upper):
171// - iwork: >= 2n StorageIndex, carved into xi | pstack (each length n).
172// - mark: >= n bytes, all-zero.
173// - xwork: >= n Scalar, the dense accumulator, zero except b scattered on bIdx.
174// `innerNonZeroPtr` is nullptr for a compressed T, or the per-column nonzero count for
175// an uncompressed T (columns then end at outerIndexPtr[j]+innerNonZeroPtr[j]).
176template <bool Upper, bool UnitDiag, typename StorageIndex, typename LhsScalar, typename RhsScalar>
177Index reach_solve_dense(const StorageIndex* outerIndexPtr, const StorageIndex* innerIndexPtr, const LhsScalar* valuePtr,
178 const StorageIndex* innerNonZeroPtr, Index n, const StorageIndex* bIdx, Index bCount,
179 StorageIndex* iwork, uint8_t* mark, RhsScalar* xwork) {
180 Index top =
181 triangular_reach<Upper>(outerIndexPtr, innerIndexPtr, innerNonZeroPtr, bIdx, bCount, iwork, iwork + n, mark, n);
182 triangular_solve_over_reach<Upper, UnitDiag>(outerIndexPtr, innerIndexPtr, valuePtr, innerNonZeroPtr, iwork, top, n,
183 xwork);
184 return top;
185}
186
187// ---------------------------------------------------------------------------
188// Iterator-driven path: for a triangular, column-major sparse expression that does NOT
189// expose raw CSC storage (has_compressed_access is false). Columns are read through the
190// expression's evaluator InnerIterator. Because an InnerIterator can't cheaply hold DFS
191// resume state, the reach uses a mark-on-push worklist plus a final sort into
192// topological order (ascending index for lower, descending for upper -- both valid
193// topological orders for the respective solve); the log factor is empirically ~free.
194// ---------------------------------------------------------------------------
195
196// Reach via a mark-on-push worklist that opens one column at a time through
197// InnerIterator. Only in-triangle successors (index > j for lower, index < j for upper)
198// are pushed. Scratch: xi (n, shared stack+output) and mark (n bytes, all-zero in,
199// reach flagged out). Returns top; xi[top..n) is sorted into the topological order for
200// the solve direction.
201template <bool Upper, typename Eval, typename StorageIndex>
202Index triangular_reach_iter(const Eval& mat, const StorageIndex* bIdx, Index bCount, StorageIndex* xi, uint8_t* mark,
203 Index n) {
204 Index top = n;
205 Index sp = 0;
206 for (Index r = 0; r < bCount; ++r) {
207 StorageIndex root = bIdx[r];
208 if (!mark[root]) {
209 mark[root] = 1;
210 xi[sp++] = root;
211 }
212 }
213 while (sp > 0) {
214 StorageIndex j = xi[--sp];
215 xi[--top] = j; // collect
216 for (typename Eval::InnerIterator it(mat, j); it; ++it) {
217 StorageIndex i = StorageIndex(it.index());
218 if (Upper ? (i >= j) : (i <= j)) continue; // out of triangle, or the diagonal
219 if (!mark[i]) {
220 mark[i] = 1;
221 xi[sp++] = i;
222 }
223 }
224 }
225 // descending for upper, ascending for lower (comparator type picked at compile time)
226 using Comp = std::conditional_t<Upper, std::greater<StorageIndex>, std::less<StorageIndex>>;
227 std::sort(xi + top, xi + n, Comp{});
228 return top;
229}
230
231// Numeric sweep over the reach, reading columns through InnerIterator. Direction-aware
232// like the pointer sweep: for lower the leading out-of-triangle entries (index < j) are
233// skipped so the diagonal is the first remaining entry; for upper the diagonal is found
234// by scan and only the entries with index < j are updated.
235template <bool Upper, bool UnitDiag, typename Eval, typename StorageIndex, typename Scalar>
236void triangular_solve_over_reach_iter(const Eval& mat, const StorageIndex* xi, Index top, Index n, Scalar* x) {
237 for (Index k = top; k < n; ++k) {
238 StorageIndex j = xi[k];
239 EIGEN_IF_CONSTEXPR (Upper) {
240 EIGEN_IF_CONSTEXPR (!UnitDiag) {
241 Scalar d(0); // stays 0 if the diagonal is missing, so singularity surfaces as inf/NaN
242 bool hasDiag = false;
243 for (typename Eval::InnerIterator dt(mat, j); dt; ++dt)
244 if (StorageIndex(dt.index()) == j) {
245 d = dt.value();
246 hasDiag = true;
247 }
248 eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
249 EIGEN_UNUSED_VARIABLE(hasDiag);
250 x[j] /= d; // d == 0 when the diagonal is missing -> inf/NaN, consistent with the pointer path
251 }
252 Scalar xj = x[j];
253 for (typename Eval::InnerIterator it(mat, j); it && StorageIndex(it.index()) < j; ++it)
254 x[it.index()] = numext::madd<Scalar>(-xj, it.value(), x[it.index()]);
255 } else {
256 typename Eval::InnerIterator it(mat, j);
257 while (it && StorageIndex(it.index()) < j) ++it; // skip out-of-triangle (index < j)
258 bool hasDiag = it && StorageIndex(it.index()) == j; // diagonal is the first in-triangle entry
259 EIGEN_IF_CONSTEXPR (!UnitDiag) {
260 eigen_assert(hasDiag && "sparse triangular solve: missing diagonal");
261 // Missing diagonal -> inf/NaN, not it.value() on an ended/wrong iterator (see pointer path).
262 x[j] /= hasDiag ? it.value() : Scalar(0);
263 }
264 if (hasDiag) ++it; // step past the stored diagonal (the divisor above, or a unit entry)
265 Scalar xj = x[j];
266 for (; it; ++it) x[it.index()] = numext::madd<Scalar>(-xj, it.value(), x[it.index()]);
267 }
268 }
269}
270
271// Iterator core: reach + numeric, returning top (no cleanup), the iterator counterpart
272// of reach_solve_dense -- xwork must already hold the scattered rhs. Uses the same 2n /
273// n(bytes) / n workspace layout (the pstack half of iwork is left unused), so the two
274// general paths share one workspace contract.
275template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
276Index reach_solve_dense_iter(const LhsType& lhs, Index n, const StorageIndex* bIdx, Index bCount, StorageIndex* iwork,
277 uint8_t* mark, Scalar* xwork) {
278 evaluator<LhsType> mat(lhs);
279 Index top = triangular_reach_iter<Upper>(mat, bIdx, bCount, iwork, mark, n);
280 triangular_solve_over_reach_iter<Upper, UnitDiag>(mat, iwork, top, n, xwork);
281 return top;
282}
283
284// Policy dispatch for the core (returns top): an expression that exposes raw storage
285// takes the pointer + DFS fast path; anything else takes the evaluator + worklist path.
286// Tag dispatch (not if-constexpr) keeps the untaken branch from being instantiated, so
287// outerIndexPtr() is never named on a type that lacks it.
288//
289// CompressedAccessBit is a compile-time capability, not a guarantee the instance is
290// compressed: an uncompressed SparseMatrix keeps per-column gaps addressed via
291// innerNonZeroPtr(), so its columns do NOT run to outerIndexPtr()[j+1]. Passing
292// innerNonZeroPtr() through keeps the raw-pointer path valid either way -- it is nullptr
293// exactly when compressed (columns end at outerIndexPtr[j+1]) and the per-column count
294// otherwise (columns end at outerIndexPtr[j]+innerNonZeroPtr[j]).
295template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
296Index reach_solve_dense_dispatch(std::true_type /*compressed*/, const LhsType& lhs, Index n, const StorageIndex* bIdx,
297 Index bCount, StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
298 return reach_solve_dense<Upper, UnitDiag>(lhs.outerIndexPtr(), lhs.innerIndexPtr(), lhs.valuePtr(),
299 lhs.innerNonZeroPtr(), n, bIdx, bCount, iwork, mark, xwork);
300}
301template <bool Upper, bool UnitDiag, typename LhsType, typename StorageIndex, typename Scalar>
302Index reach_solve_dense_dispatch(std::false_type /*iterator*/, const LhsType& lhs, Index n, const StorageIndex* bIdx,
303 Index bCount, StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
304 return reach_solve_dense_iter<Upper, UnitDiag>(lhs, n, bIdx, bCount, iwork, mark, xwork);
305}
306
307// Expression core: solve T x = b for a sparse-expression triangular T with the rhs
308// PRE-SCATTERED into xwork (bIdx is its pattern), selecting the pointer or iterator path
309// at compile time; RETURNS top with the solution left in xwork and the reach in
310// iwork[top..n) (see reach_solve_dense). This is what the sparse selector uses -- it
311// scatters the rhs as it reads it and consumes xwork directly, so no bVal/outIdx/outVal.
312template <bool Upper, bool UnitDiag, typename LhsDerived, typename StorageIndex, typename Scalar>
313Index reach_solve_dense(const SparseMatrixBase<LhsDerived>& lhs, const StorageIndex* bIdx, Index bCount,
314 StorageIndex* iwork, uint8_t* mark, Scalar* xwork) {
315 return reach_solve_dense_dispatch<Upper, UnitDiag>(bool_constant<has_compressed_access<LhsDerived>::value>{},
316 lhs.derived(), lhs.rows(), bIdx, bCount, iwork, mark, xwork);
317}
318
319} // namespace internal
320
321} // namespace Eigen
322
323#endif // EIGEN_TRIANGULAR_REACH_SOLVER_H
@ UnitDiag
Definition Constants.h:216
@ Upper
Definition Constants.h:214