LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
ctmc_bicgstab.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_MC_CTMC_BICGSTAB_H
6#define LINE_API_MC_CTMC_BICGSTAB_H
7
8/**
9 * @file
10 * @ingroup api_mc
11 * Preconditioned stabilized biconjugate gradients, for the linear systems a
12 * generator produces.
13 *
14 * Templated port of matlab/src/api/mc/ctmc_bicgstab.m and
15 * jar/src/main/java/jline/api/mc/Ctmc_bicgstab.java. This is the
16 * short-recurrence counterpart of ctmc_gmres: work and storage per iteration are
17 * constant rather than growing with the Krylov dimension, so the method does not
18 * restart and does not lose the optimality that restarting costs GMRES. Where
19 * GMRES(m) stagnates because the useful subspace is wider than m, this
20 * converges; where it does not, GMRES(m) is the more robust of the two, hence
21 * the order in which ctmc_solve tries them.
22 *
23 * The equilibration, the reverse Cuthill-McKee reordering and the ILUT
24 * preconditioner are those of ctmc_gmres, reused through detail::GmresPrepared
25 * rather than reimplemented, so both methods factorize the same matrix in the
26 * same order and a switch between them cannot move a reported metric for a
27 * reason other than the iteration itself.
28 *
29 * The preconditioner is applied on the RIGHT, on the search directions p and s,
30 * so the recurrence carries the residual of the ORIGINAL system and the
31 * convergence test needs no unpreconditioning. This matches the choice made in
32 * ctmc_gmres and, as there, means the reported relres is the true relative
33 * residual rather than MATLAB's preconditioned one.
34 *
35 * FLAG follows the MATLAB bicgstab convention: 0 converged, 1 iteration limit,
36 * 3 stagnation or divergence, 4 a scalar quantity became too small or too large
37 * to continue. ITER counts matrix-vector products with A: two per complete
38 * iteration, and one when the iteration converges at its half step, so an ODD
39 * count is normal. Counting products rather than iterations is what makes it
40 * comparable with the ITER of ctmc_gmres and across the four codebases.
41 *
42 * GATED ON TRANSCENDENTAL ARITHMETIC, for the reason given in ctmc_gmres: the
43 * iteration stops on a residual tolerance and normalizes by a Euclidean norm,
44 * so at Rational it would run to the iteration limit with exploding
45 * denominators. The exact solve of the same system is ctmc_solve.
46 */
47
48#include <algorithm>
49#include <cmath>
50#include <cstddef>
51#include <vector>
52
54#include "line/num/number.h"
55#include "line/util/error.h"
56#include "line/util/matrix.h"
57
58namespace line {
59namespace mc {
60
61template <class T>
63 std::vector<T> x; ///< solution
64 int flag; ///< 0 converged, 1 iteration limit, 3 stagnation, 4 breakdown
65 T relres; ///< true relative residual norm(b - A x) / norm(b)
66 long iter; ///< matrix-vector products with A
67};
68
69template <class T>
71 Matrix<T> X; ///< solution block, empty unless flag == 0
72 int flag; ///< 0 all columns converged, otherwise the first failing flag
73};
74
75namespace detail {
76
77constexpr double BICGSTAB_DEFAULT_TOL = 1e-12;
78/** Complete iterations allowed by default; storage is O(n) whatever the count. */
79constexpr long BICGSTAB_DEFAULT_MAXIT = 200;
80/** Threshold below which rho or omega is treated as a Lanczos breakdown. */
81constexpr double BICGSTAB_BREAKDOWN_TOL = 1e-14;
82
83/** Right-preconditioned BiCGSTAB on an already prepared system. */
84template <class T>
85BicgstabResult<T> bicgstab_solve(const GmresPrepared<T>& prep, const std::vector<T>& rhsIn,
86 const std::vector<T>& x0In, double tol, long maxit) {
87 const std::size_t n = prep.n;
88 const T zero = num_traits<T>::from_int(0);
89 const T one = num_traits<T>::from_int(1);
90 if (tol <= 0.0) tol = BICGSTAB_DEFAULT_TOL;
91 if (maxit <= 0) maxit = std::min(static_cast<long>(n), BICGSTAB_DEFAULT_MAXIT);
92 maxit = std::max(1L, std::min(maxit, static_cast<long>(n)));
93 const T tolT = num_traits<T>::from_double(tol);
94 const T breakT = num_traits<T>::from_double(BICGSTAB_BREAKDOWN_TOL);
95
96 // Row scaling, then RCM permutation, on both the right-hand side and the
97 // initial guess, exactly as gmres_solve does.
98 std::vector<T> rhs(n), x(n);
99 for (std::size_t i = 0; i < n; ++i) rhs[i] = rhsIn[prep.perm[i]] / prep.rowScale[prep.perm[i]];
100 for (std::size_t i = 0; i < n; ++i) x[i] = x0In[prep.perm[i]];
101
102 T bnorm = vec_norm2(rhs);
103 if (bnorm == zero) bnorm = one;
104
106 out.flag = 1;
107 out.iter = 0;
108
109 std::vector<T> r(n);
110 prep.csr.mult(x, r);
111 for (std::size_t i = 0; i < n; ++i) r[i] = rhs[i] - r[i];
112 out.relres = vec_norm2(r) / bnorm;
113 if (out.relres <= tolT) {
114 out.x.assign(n, zero);
115 for (std::size_t i = 0; i < n; ++i) out.x[prep.perm[i]] = x[i];
116 out.flag = 0;
117 return out;
118 }
119
120 // The shadow residual is fixed at the initial residual, the standard choice:
121 // any vector not orthogonal to r would do, and this one cannot be.
122 const std::vector<T> rhat = r;
123 std::vector<T> p(n, zero), v(n, zero), s(n, zero), t(n, zero), ph(n, zero), sh(n, zero);
124
125 T rho = one, alpha = one, omega = one;
126 T bestrelres = out.relres;
127
128 for (long it = 0; it < maxit; ++it) {
129 const T rhoNew = vec_dot(rhat, r);
130 // rho vanishing is the biorthogonality breakdown of the underlying
131 // Lanczos process, not slow convergence: restarting with a fresh shadow
132 // vector would discard the iterate, so the caller is told to use another
133 // method instead.
134 if (num_abs(rhoNew) <= breakT * vec_norm2(rhat) * vec_norm2(r)) {
135 out.flag = 4;
136 break;
137 }
138 if (it == 0) {
139 p = r;
140 } else {
141 if (omega == zero) {
142 out.flag = 4;
143 break;
144 }
145 const T beta = (rhoNew / rho) * (alpha / omega);
146 for (std::size_t i = 0; i < n; ++i) p[i] = r[i] + beta * (p[i] - omega * v[i]);
147 }
148 rho = rhoNew;
149
150 prep.M.apply(p, ph);
151 prep.csr.mult(ph, v);
152 ++out.iter;
153
154 const T rhatv = vec_dot(rhat, v);
155 if (rhatv == zero || !num_isfinite(rhatv)) {
156 out.flag = 4;
157 break;
158 }
159 alpha = rho / rhatv;
160
161 for (std::size_t i = 0; i < n; ++i) s[i] = r[i] - alpha * v[i];
162
163 // Half-step convergence: s is the residual of x + alpha*ph, so a
164 // converged s reaches the answer without the second matvec.
165 const T snorm = vec_norm2(s);
166 if (snorm / bnorm <= tolT) {
167 for (std::size_t i = 0; i < n; ++i) x[i] += alpha * ph[i];
168 out.relres = snorm / bnorm;
169 out.flag = 0;
170 break;
171 }
172
173 prep.M.apply(s, sh);
174 prep.csr.mult(sh, t);
175 ++out.iter;
176
177 const T tt = vec_dot(t, t);
178 if (tt == zero || !num_isfinite(tt)) {
179 out.flag = 4;
180 break;
181 }
182 omega = vec_dot(t, s) / tt;
183
184 for (std::size_t i = 0; i < n; ++i) x[i] += alpha * ph[i] + omega * sh[i];
185 for (std::size_t i = 0; i < n; ++i) r[i] = s[i] - omega * t[i];
186
187 out.relres = vec_norm2(r) / bnorm;
188 if (out.relres <= tolT) {
189 out.flag = 0;
190 break;
191 }
192 // omega vanishing stalls the update of x while leaving r finite, so the
193 // iteration would spin without progress.
194 if (num_abs(omega) <= breakT) {
195 out.flag = 4;
196 break;
197 }
198 // BiCGSTAB residuals are non-monotone by construction, so an increase is
199 // not by itself stagnation and the test is against the BEST residual
200 // seen rather than the previous one. Growing two orders of magnitude
201 // past that best is divergence.
202 if (out.relres > num_traits<T>::from_int(100) * bestrelres) {
203 out.flag = 3;
204 break;
205 }
206 if (out.relres < bestrelres) bestrelres = out.relres;
207 }
208
209 out.x.assign(n, zero);
210 for (std::size_t i = 0; i < n; ++i) out.x[prep.perm[i]] = x[i];
211 for (std::size_t i = 0; i < n; ++i)
212 if (!num_isfinite(out.x[i])) {
213 out.flag = 4;
215 return out;
216 }
217 if (out.relres <= tolT) out.flag = 0;
218 return out;
219}
220
221} // namespace detail
222
223/**
224 * @brief Preconditioned stabilized biconjugate gradients, for the linear
225 * systems a generator produces.
226 *
227 * @param A coefficient matrix, already assembled
228 * @param b right-hand side
229 * @param tol relative residual tolerance (default 1e-12)
230 * @param maxit complete iterations; <= 0 selects min(n, 200)
231 * @param x0 initial guess; empty selects the uniform vector ones(n)/n
232 */
233template <class T>
234BicgstabResult<T> ctmc_bicgstab(const Matrix<T>& A, const std::vector<T>& b, double tol = 1e-12,
235 long maxit = 0, const std::vector<T>& x0 = std::vector<T>()) {
237 "ctmc_bicgstab requires transcendental arithmetic: the iteration stops on a "
238 "residual tolerance and normalizes by a Euclidean norm, so there is no exact "
239 "result to converge to; use ctmc_solve for an exact solve");
240 const std::size_t n = A.rows();
241 if (A.cols() != n) throw InputError("ctmc_bicgstab: matrix is not square");
242 if (b.size() != n) throw InputError("ctmc_bicgstab: right-hand side has the wrong length");
243 if (!x0.empty() && x0.size() != n) throw InputError("ctmc_bicgstab: initial guess has the wrong length");
244 std::vector<T> guess = x0;
245 if (guess.empty())
246 guess.assign(n, num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(n)));
247 const detail::GmresPrepared<T> prep(A);
248 return detail::bicgstab_solve(prep, b, guess, tol, maxit);
249}
250
251/**
252 * Every column of B solved against the SAME equilibration, reordering and ILUT
253 * factorization, each column starting from the previous column's solution. FLAG
254 * is zero only when every column converged; on any other value the result matrix
255 * is empty and the caller must fall back, a partially converged block leaving
256 * the fallback ambiguous.
257 *
258 * @param A coefficient matrix
259 * @param B right-hand sides, one per column
260 * @param tol relative residual tolerance (default 1e-12)
261 * @param maxit complete iterations per column; <= 0 selects min(n, 200)
262 */
263template <class T>
264BicgstabMultiResult<T> ctmc_bicgstab_multi(const Matrix<T>& A, const Matrix<T>& B, double tol = 1e-12,
265 long maxit = 0) {
267 "ctmc_bicgstab_multi requires transcendental arithmetic: see ctmc_bicgstab, the "
268 "iteration stops on a residual tolerance rather than reaching an exact value");
269 const std::size_t n = A.rows();
270 if (A.cols() != n) throw InputError("ctmc_bicgstab_multi: matrix is not square");
271 if (B.rows() != n) throw InputError("ctmc_bicgstab_multi: right-hand side block has the wrong height");
272
273 const detail::GmresPrepared<T> prep(A);
274 const std::size_t nrhs = B.cols();
275 Matrix<T> X(n, nrhs, num_traits<T>::from_int(0));
276 std::vector<T> guess(n, num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(n)));
277 std::vector<T> rhs(n);
278
280 for (std::size_t c = 0; c < nrhs; ++c) {
281 for (std::size_t i = 0; i < n; ++i) rhs[i] = B(i, c);
282 const BicgstabResult<T> r = detail::bicgstab_solve(prep, rhs, guess, tol, maxit);
283 if (r.flag != 0) {
284 out.X = Matrix<T>();
285 out.flag = r.flag;
286 return out;
287 }
288 for (std::size_t i = 0; i < n; ++i) X(i, c) = r.x[i];
289 guess = r.x;
290 }
291 out.X = X;
292 out.flag = 0;
293 return out;
294}
295
296} // namespace mc
297} // namespace line
298
299#endif // LINE_API_MC_CTMC_BICGSTAB_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
Restarted GMRES with an ILUT preconditioner, for the linear systems a generator produces.
The exception types the port throws.
Dense matrix and non-owning view.
BicgstabResult< T > ctmc_bicgstab(const Matrix< T > &A, const std::vector< T > &b, double tol=1e-12, long maxit=0, const std::vector< T > &x0=std::vector< T >())
Preconditioned stabilized biconjugate gradients, for the linear systems a generator produces.
BicgstabMultiResult< T > ctmc_bicgstab_multi(const Matrix< T > &A, const Matrix< T > &B, double tol=1e-12, long maxit=0)
Every column of B solved against the SAME equilibration, reordering and ILUT factorization,...
T num_abs(const T &v)
Definition number.h:172
Number-type abstraction for the templated API port.
int flag
0 all columns converged, otherwise the first failing flag
Matrix< T > X
solution block, empty unless flag == 0
T relres
true relative residual norm(b - A x) / norm(b)
std::vector< T > x
solution
int flag
0 converged, 1 iteration limit, 3 stagnation, 4 breakdown
long iter
matrix-vector products with A