1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
|
export module linalgebra:precond;
import std;
import :error;
import :vector;
import :matrix;
import :norms;
import :triangular_solve;
import :lu;
import :qr;
// References used throughout this file:
// T&B — Trefethen & Bau, "Numerical Linear Algebra"
// GVL — Golub & Van Loan, "Matrix Computations" 4th ed.
// Hager — Hager, W.W. (1984), SIAM J. Sci. Stat. Comput. 5(2):311-316
export namespace linalgebra {
// Condition number estimation (1-norm, Hager/LINPACK power iteration)
// Reference: Hager (1984); GVL §2.3.3
[[nodiscard]] double condition_number_1norm(const Matrix& A,
double singular_tolerance = 1e-12);
// Jacobi (diagonal) preconditioner
struct JacobiPrecond {
Vector inv_diag;
};
[[nodiscard]] JacobiPrecond precond_jacobi(const Matrix& A,
double zero_tolerance = 1e-14);
[[nodiscard]] Vector apply(const JacobiPrecond& P, const Vector& x);
// ILU(0) preconditioner (for dense matrices = LU without pivoting)
// Reference: Saad, "Iterative Methods for Sparse Linear Systems" §10.3
struct ILU0Precond {
Matrix LU; // combined: strict lower = L multipliers, upper = U
};
[[nodiscard]] ILU0Precond precond_ilu0(const Matrix& A,
double zero_tolerance = 1e-14);
[[nodiscard]] Vector apply(const ILU0Precond& P, const Vector& b);
// Least-squares solver via column-pivoting QR
// Reference: T&B Lecture 11; GVL §5.5
struct LstsqResult {
Vector x;
std::size_t rank;
double residual_norm;
};
struct LstsqOptions {
double rank_tolerance = 1e-12;
};
[[nodiscard]] LstsqResult lstsq(const Matrix& A, const Vector& b,
LstsqOptions opts = {});
} // namespace linalgebra
namespace {
// Solve A^T z = rhs given a pre-computed LU factorization of A.
// PA = LU => A^T = U^T L^T P
// Steps: (1) solve U^T q = rhs, (2) solve L^T w = q, (3) z[perm[i]] = w[i]
linalgebra::Vector solve_transpose(const linalgebra::LUResult& lu,
const linalgebra::Vector& rhs) {
const std::size_t n = lu.L.rows();
// Solve U^T q = rhs (U^T is lower triangular)
const linalgebra::Matrix Ut = linalgebra::transpose(lu.U);
const linalgebra::Vector q = linalgebra::forward_substitution(Ut, rhs);
// Solve L^T w = q (L^T is upper triangular, unit diagonal)
const linalgebra::Matrix Lt = linalgebra::transpose(lu.L);
const linalgebra::Vector w = linalgebra::backward_substitution(Lt, q, 1e-14, true);
// Apply inverse permutation: z[perm[i]] = w[i]
linalgebra::Vector z(n);
for (std::size_t i = 0; i < n; ++i) z[lu.perm[i]] = w[i];
return z;
}
} // namespace
namespace linalgebra {
double condition_number_1norm(const Matrix& A, double singular_tolerance) {
if (A.rows() != A.cols()) {
std::ostringstream oss;
oss << "condition_number_1norm requires a square matrix, got "
<< A.rows() << "x" << A.cols();
throw DimensionMismatchError(oss.str());
}
const std::size_t n = A.rows();
// Exact 1-norm of A: max column sum of absolute values.
double norm_A = 0.0;
for (std::size_t j = 0; j < n; ++j) {
double col_sum = 0.0;
for (std::size_t i = 0; i < n; ++i) col_sum += std::abs(A(i, j));
norm_A = std::max(norm_A, col_sum);
}
const LUResult lu = lu_factor(A, singular_tolerance);
// Estimate ||A^{-1}||_1 via the power-iteration method (Hager 1984).
// Start with x = [1/n, ..., 1/n].
Vector x(n, 1.0 / static_cast<double>(n));
double est = 0.0;
for (int iter = 0; iter < 5; ++iter) {
const Vector y = lu_solve(lu, x); // y = A^{-1} x
// 1-norm of y.
double y1 = 0.0;
for (std::size_t i = 0; i < n; ++i) y1 += std::abs(y[i]);
if (y1 <= est) break;
est = y1;
Vector xi(n);
for (std::size_t i = 0; i < n; ++i) xi[i] = (y[i] >= 0.0) ? 1.0 : -1.0;
// z = A^{-T} xi
const Vector z = solve_transpose(lu, xi);
// Find the index maximizing |z[j]|.
std::size_t j_max = 0;
double max_z = std::abs(z[0]);
for (std::size_t i = 1; i < n; ++i) {
if (std::abs(z[i]) > max_z) {
max_z = std::abs(z[i]);
j_max = i;
}
}
// Convergence check.
double xz = 0.0;
for (std::size_t i = 0; i < n; ++i) xz += std::abs(z[i]) / static_cast<double>(n);
if (max_z <= xz) break;
// New starting vector: e_{j_max}.
x.fill(0.0);
x[j_max] = 1.0;
}
return norm_A * est;
}
JacobiPrecond precond_jacobi(const Matrix& A, double zero_tolerance) {
if (A.rows() != A.cols()) {
std::ostringstream oss;
oss << "precond_jacobi requires a square matrix, got "
<< A.rows() << "x" << A.cols();
throw DimensionMismatchError(oss.str());
}
const std::size_t n = A.rows();
Vector inv_diag(n);
for (std::size_t i = 0; i < n; ++i) {
if (std::abs(A(i, i)) <= zero_tolerance) {
std::ostringstream oss;
oss << "precond_jacobi: zero diagonal entry at index " << i;
throw SingularMatrixError(oss.str());
}
inv_diag[i] = 1.0 / A(i, i);
}
return JacobiPrecond{std::move(inv_diag)};
}
Vector apply(const JacobiPrecond& P, const Vector& x) {
const std::size_t n = x.size();
if (n != P.inv_diag.size()) {
throw DimensionMismatchError("apply(JacobiPrecond): size mismatch");
}
Vector result(n);
for (std::size_t i = 0; i < n; ++i) result[i] = P.inv_diag[i] * x[i];
return result;
}
ILU0Precond precond_ilu0(const Matrix& A, double zero_tolerance) {
if (A.rows() != A.cols()) {
std::ostringstream oss;
oss << "precond_ilu0 requires a square matrix, got "
<< A.rows() << "x" << A.cols();
throw DimensionMismatchError(oss.str());
}
const std::size_t n = A.rows();
Matrix LU = A;
for (std::size_t k = 0; k < n; ++k) {
if (std::abs(LU(k, k)) <= zero_tolerance) {
std::ostringstream oss;
oss << "precond_ilu0: near-zero pivot at step " << k;
throw SingularMatrixError(oss.str());
}
for (std::size_t i = k + 1; i < n; ++i) {
LU(i, k) /= LU(k, k);
for (std::size_t j = k + 1; j < n; ++j) {
LU(i, j) -= LU(i, k) * LU(k, j);
}
}
}
return ILU0Precond{std::move(LU)};
}
Vector apply(const ILU0Precond& P, const Vector& b) {
const std::size_t n = P.LU.rows();
if (b.size() != n) {
throw DimensionMismatchError("apply(ILU0Precond): size mismatch");
}
// Extract L (unit lower) and U (upper) from combined storage.
Matrix L = Matrix::zeros(n, n);
Matrix U = Matrix::zeros(n, n);
for (std::size_t i = 0; i < n; ++i) {
L(i, i) = 1.0;
for (std::size_t j = 0; j < i; ++j) L(i, j) = P.LU(i, j);
for (std::size_t j = i; j < n; ++j) U(i, j) = P.LU(i, j);
}
const Vector y = forward_substitution(L, b, 1e-14, true);
return backward_substitution(U, y);
}
LstsqResult lstsq(const Matrix& A, const Vector& b, LstsqOptions opts) {
const std::size_t m = A.rows();
const std::size_t n = A.cols();
if (m < n) {
std::ostringstream oss;
oss << "lstsq requires rows >= cols, got " << m << "x" << n;
throw DimensionMismatchError(oss.str());
}
if (b.size() != m) {
std::ostringstream oss;
oss << "lstsq: rhs size " << b.size() << " does not match rows " << m;
throw DimensionMismatchError(oss.str());
}
const QRColPivResult qr = qr_colpiv(A, opts.rank_tolerance);
const std::size_t r = qr.rank;
// c = Q^T b (Q is m×n with orthonormal columns)
Vector c(n, 0.0);
for (std::size_t j = 0; j < n; ++j) {
double d = 0.0;
for (std::size_t i = 0; i < m; ++i) d += qr.Q(i, j) * b[i];
c[j] = d;
}
Vector x(n, 0.0);
if (r > 0) {
Matrix Rr(r, r);
for (std::size_t i = 0; i < r; ++i)
for (std::size_t j = 0; j < r; ++j)
Rr(i, j) = qr.R(i, j);
Vector cr(r);
for (std::size_t i = 0; i < r; ++i) cr[i] = c[i];
const Vector y = backward_substitution(Rr, cr);
// Permuted solution: x_perm[0:r] = y, x_perm[r:n] = 0, then un-permute.
for (std::size_t j = 0; j < r; ++j) x[qr.perm[j]] = y[j];
}
const Vector res = A * x - b;
const double residual_norm = norm2(res);
return LstsqResult{std::move(x), r, residual_norm};
}
} // namespace linalgebra
|