numerics
Loading...
Searching...
No Matches
qr.cpp
Go to the documentation of this file.
2#include <cmath>
3#include <algorithm>
4#include <vector>
5
6namespace num {
7
8// qr() -- Householder reflections
9//
10// A Householder reflector is H = I - 2*v*v^T where v is a unit vector.
11// H is symmetric and orthogonal: H^T = H, H^2 = I, det(H) = -1.
12//
13// For a vector x, choose v so that H*x = -sign(x[0])*||x||*e_1.
14// This zeros all entries of x below the first.
15//
16// SIGN CONVENTION
17// v = x + sign(x[0]) * ||x|| * e_1
18//
19// Using the same sign as x[0] avoids catastrophic cancellation.
20// If x[0] > 0: v[0] = x[0] + ||x|| (sum of positives -- well conditioned)
21// If x[0] < 0: v[0] = x[0] - ||x|| (sum of negatives -- still fine)
22// Contrast with v[0] = x[0] - ||x|| when x[0] > 0: cancellation when x[0] ~= ||x||.
23//
24// APPLYING H TO A SUBMATRIX (RANK-1 UPDATE)
25// H * A[k:m, k:n] = A[k:m, k:n] - 2*v*(v^T * A[k:m, k:n])
26//
27// This is an outer product update: one inner product per column, one axpy.
28// Cost: O(m*n) per step. Total cost: O(m*n^2 - n^3/3).
29//
30// BUILDING Q EXPLICITLY
31// Q = H_0 * H_1 * ... * H_{r-1} where r = min(m-1, n)
32//
33// Build by applying reflectors in reverse order to the identity:
34// Q = I
35// for k = r-1 downto 0:
36// Q[k:m, k:m] = H_k * Q[k:m, k:m] (only columns >= k are non-zero)
37//
38// Correctness: After applying H_{r-1},...,H_{k+1}, Q[:,j<k] = e_j (untouched
39// by all previous steps since H_i only acts on rows >= i > j).
40
42 constexpr real householder_tol = 1e-14;
43 const idx m = A.rows();
44 const idx n = A.cols();
45 const idx r = (m > n) ? n : m - 1; // number of Householder steps
46
47 Matrix R = A; // working copy -> becomes R at the end
48
49 // Store the normalised Householder vectors for later Q accumulation.
50 // vs[k] has length m-k; it spans the rows k..m-1.
51 std::vector<std::vector<real>> vs(r);
52
53 for (idx k = 0; k < r; ++k) {
54 const idx len = m - k;
55
56 std::vector<real> x(len);
57 for (idx i = 0; i < len; ++i) x[i] = R(k + i, k);
58
59 real norm_x = real(0);
60 for (idx i = 0; i < len; ++i) norm_x += x[i] * x[i];
61 norm_x = std::sqrt(norm_x);
62
63 std::vector<real> v = x;
64 v[0] += (x[0] >= real(0)) ? norm_x : -norm_x; // sign trick
65
66 real norm_v = real(0);
67 for (idx i = 0; i < len; ++i) norm_v += v[i] * v[i];
68 norm_v = std::sqrt(norm_v);
69
70 if (norm_v < householder_tol) {
71 // Column already zero below diagonal; Householder is identity.
72 vs[k].assign(len, real(0));
73 continue;
74 }
75 for (idx i = 0; i < len; ++i) v[i] /= norm_v;
76 vs[k] = v;
77
78 // For each column j: R[k:m, j] -= 2 * v * (v^T * R[k:m, j])
79 for (idx j = k; j < n; ++j) {
80 real vTr = real(0);
81 for (idx i = 0; i < len; ++i) vTr += v[i] * R(k + i, j);
82 const real two_vTr = real(2) * vTr;
83 for (idx i = 0; i < len; ++i) R(k + i, j) -= two_vTr * v[i];
84 }
85 }
86
87 Matrix Q(m, m, real(0));
88 for (idx i = 0; i < m; ++i) Q(i, i) = real(1); // start with identity
89
90 for (idx k = r; k-- > 0; ) {
91 const std::vector<real>& v = vs[k];
92 const idx len = static_cast<idx>(v.size());
93
94 // Apply H_k to Q[k:m, k:m]: Q[k:m, j] -= 2*v*(v^T * Q[k:m, j])
95 // Only columns j >= k can be non-zero at this point (see note above).
96 for (idx j = k; j < m; ++j) {
97 real vTq = real(0);
98 for (idx i = 0; i < len; ++i) vTq += v[i] * Q(k + i, j);
99 const real two_vTq = real(2) * vTq;
100 for (idx i = 0; i < len; ++i) Q(k + i, j) -= two_vTq * v[i];
101 }
102 }
103
104 for (idx i = 1; i < m; ++i)
105 for (idx j = 0; j < std::min(i, n); ++j)
106 R(i, j) = real(0);
107
108 return {std::move(Q), std::move(R)};
109}
110
111// qr_solve() -- least-squares via Q^T multiply + back-substitution
112//
113// min ||A*x - b|| <=> R[:n,:n] * x = (Q^T * b)[:n]
114//
115// Proof: ||A*x - b||^2 = ||Q*R*x - b||^2 = ||R*x - Q^T*b||^2
116// = ||R[:n,:n]*x - c[:n]||^2 + ||c[n:]||^2
117// where c = Q^T*b. The second term is fixed (residual); minimise the first.
118
119void qr_solve(const QRResult& f, const Vector& b, Vector& x) {
120 const idx m = f.Q.rows();
121 const idx n = f.R.cols();
122
123 // Q^T[i,j] = Q[j,i] so y[i] = sum_j Q[j,i] * b[j]
124 Vector y(m, real(0));
125 for (idx i = 0; i < m; ++i)
126 for (idx j = 0; j < m; ++j)
127 y[i] += f.Q(j, i) * b[j];
128
129 Vector xv(n, real(0));
130 for (idx i = n; i-- > 0; ) {
131 xv[i] = y[i];
132 for (idx j = i + 1; j < n; ++j)
133 xv[i] -= f.R(i, j) * xv[j];
134 xv[i] /= f.R(i, i);
135 }
136
137 x = std::move(xv);
138}
139
140} // namespace num
Dense row-major matrix with optional GPU storage.
Definition matrix.hpp:12
double real
Definition types.hpp:10
void qr_solve(const QRResult &f, const Vector &b, Vector &x)
Solve the least-squares problem min ||A*x - b||_2.
Definition qr.cpp:119
constexpr T ipow(T x) noexcept
Compute x^N at compile time via repeated squaring.
std::size_t idx
Definition types.hpp:11
QRResult qr(const Matrix &A)
QR factorization of an mxn matrix A (m >= n) via Householder reflections.
Definition qr.cpp:41
constexpr real e
Definition math.hpp:41
QR factorization via Householder reflections.
Result of a QR factorization: A = Q * R.
Definition qr.hpp:18