// This #ifndef section makes clangd happy so that it can properly do type hints // in this file #ifndef QR_H_ #define QR_H_ #include "QR.hpp" #endif #ifdef QR_H_ // since the .cpp file has to be included by the .hpp file this // will evaluate to true #include "QR.hpp" #include #include namespace QR { // ============================================================================ // QR Building Block Implementations (fully templated, heap-free) // ============================================================================ /** * GivensRotation: R * (a, b)^T = (r, 0)^T with R = [[c, s], [-s, c]], * r = +hypot(a, b), c = a/r, s = b/r. */ // [[maybe_unused]]: this helper is only referenced from template // (EigenQR/Tridiagonalize), so in translation units that include this file // but never instantiate those templates, the definition is legitimately // unused. The attribute silences -Wunused-function there without hiding // real dead code in TUs that do use the algorithm. [[maybe_unused]] static void GivensRotation(float a, float b, float &c, float &s) { float r = sqrtf(a * a + b * b); if (r == 0.0f) { c = 1.0f; s = 0.0f; return; } c = a / r; s = b / r; } /** * ApplyRotationBothSides: A <- G A G^T (similarity transform) with * G = [[c, s], [-s, c]] on the (i, i+1) block, i.e. G is the ZEROING * rotation G*(x, y)^T = (r, 0)^T (the orientation used by the implicit QR * chase: A = Q R with Q = G^T gives the next iterate R Q = G A G^T). * With (c, s) = GivensRotation(A[i][i], A[i+1][i]) this zeroes * A[i+1][i] after the LEFT multiplication; the right multiplication then * chases the bulge along the superdiagonal (tridiagonal chase). * * A must be symmetric on entry; the result stays symmetric, so both * triangles are written. * * Block updates (with a00 = A[i][i], a01 = A[i][i+1], a11 = A[i+1][i+1]): * A[i][i] = c^2 a00 + 2 c s a01 + s^2 a11 * A[i][i+1] = (c^2 - s^2) a01 + c s (a11 - a00) * A[i+1][i+1] = s^2 a00 - 2 c s a01 + c^2 a11 * Off-block updates (uniform for both sides, since the left factor G and * the right factor G^T mix each side with the pattern (a, b) -> (c a + s b, * -s a + c b) after transposition): * for j not in {i, i+1}: * A[i][j] = A[j][i] = c A[i][j] + s A[i+1][j] * A[i+1][j] = A[j][i+1] = -s A[i][j] + c A[i+1][j] */ template static void ApplyRotationBothSides(Matrix &A, uint8_t i, float c, float s) { float a00 = A.Get(i, i); float a01 = A.Get(i, i + 1); float a11 = A.Get(i + 1, i + 1); float c2 = c * c; float s2 = s * s; float cs = c * s; A[i][i] = c2 * a00 + 2.0f * cs * a01 + s2 * a11; A[i][i + 1] = (c2 - s2) * a01 + cs * (a11 - a00); A[i + 1][i + 1] = s2 * a00 - 2.0f * cs * a01 + c2 * a11; A[i + 1][i] = A[i][i + 1]; // keep both triangles in sync for (uint8_t j = 0; j < N; ++j) { if (j == i || j == i + 1) continue; float x = A.Get(i, j); float y = A.Get(i + 1, j); A[i][j] = c * x + s * y; A[j][i] = A[i][j]; A[i + 1][j] = -s * x + c * y; A[j][i + 1] = A[i + 1][j]; } } /** * ApplyRotationToVectors: V <- V G^T with G = [[c, s], [-s, c]] on columns * (i, i+1), applied to every row. G^T = [[c, -s], [s, c]], so * V[r][i] <- c V[r][i] + s V[r][i+1] * V[r][i+1] <- -s V[r][i] + c V[r][i+1] * * Convention pairing: if A evolves as A <- G A G^T (ApplyRotationBothSides * with the SAME c, s), then V accumulates V <- V G^T. With V0 = I the * invariant A0 = V A V^T is preserved at every step, so at convergence * A0 = V D V^T and the columns of V are the eigenvectors. (Rationale: * each chase step is A <- R Q with R = G A the upper-triangular factor and * Q = G^T the orthogonal factor of A = Q R, so A = G^T A' G and the * orthogonal factors multiply as G1^T G2^T ... in application order.) */ template static void ApplyRotationToVectors(Matrix &V, uint8_t i, float c, float s) { for (uint8_t r = 0; r < N; ++r) { float x = V.Get(r, i); float y = V.Get(r, i + 1); V[r][i] = c * x + s * y; V[r][i + 1] = -s * x + c * y; } } /** * WilkinsonShift: eigenvalue of [[a, b], [b, d]] closest to d. * mu = (a+d)/2 - sign(a-d) * sqrt(((a-d)/2)^2 + b^2), sign(0) = +1. */ [[maybe_unused]] static float WilkinsonShift(float a, float b, float d) { float delta = 0.5f * (a - d); float spread = sqrtf(delta * delta + b * b); return 0.5f * (a + d) - (delta >= 0.0f ? spread : -spread); } /** * Solve2x2Eigen: closed-form eigen-decomposition of the 2x2 block at * (lo, lo+1). Works for symmetric blocks and for general 2x2 blocks with * real eigenvalues (used by the N == 2 entry point). * * lambdaHi/lambdaLo come from the characteristic polynomial * lambda^2 - trace*lambda + det = 0. * The eigenvector for lambdaHi is v = (b, lambdaHi - a) (from the first * row of (A - lambda*I)v = 0), normalized to unit length. If b == 0 the * block is triangular and the eigenvectors are coordinate vectors: * e1 for the larger of {a, d}, e2 for the other. */ template static void Solve2x2Eigen(const Matrix &A, uint8_t lo, float &lambdaHi, float &lambdaLo, float &c, float &s) { float a = A.Get(lo, lo); float b = A.Get(lo, lo + 1); float e = A.Get(lo + 1, lo); float d = A.Get(lo + 1, lo + 1); float trace = a + d; float det = a * d - b * e; float disc = trace * trace - 4.0f * det; if (disc < 0.0f) disc = 0.0f; // round-off clamp: real 2x2 blocks have disc >= 0 float sqrtDisc = sqrtf(disc); lambdaHi = 0.5f * (trace + sqrtDisc); lambdaLo = 0.5f * (trace - sqrtDisc); if (b != 0.0f) { float v1 = lambdaHi - a; float n = sqrtf(b * b + v1 * v1); c = b / n; s = v1 / n; } else if (a >= d) { c = 1.0f; // e1 is the eigenvector of a = lambdaHi s = 0.0f; } else { c = 0.0f; // e2 is the eigenvector of d = lambdaHi s = 1.0f; } } /** * Deflate: zero subdiagonal entries i in [lo, hi) whose magnitude is at or * below tolerance * (|A[i][i]| + |A[i+1][i+1]|). */ template static void Deflate(Matrix &A, uint8_t lo, uint8_t hi, float tolerance) { for (uint8_t i = lo; i < hi; ++i) { float t = A.Get(i + 1, i); float scale = fabsf(A.Get(i, i)) + fabsf(A.Get(i + 1, i + 1)); if (fabsf(t) <= tolerance * scale) { A[i + 1][i] = 0.0f; A[i][i + 1] = 0.0f; } } } // ============================================================================ // QR::EigenQR driver (implicit Wilkinson-shifted QR, bulge chasing) // ============================================================================ /** * Tridiagonalize: Givens tridiagonalization (Golub & Van Loan 8.3.1). * * For column k = 0..N-3 the entries A[k+2..N-1, k] are eliminated by * rotations on (i, i+1) applied BOTTOM-UP, i = N-2 down to k+1, each * formed from the CURRENT (already-updated) pair (A[i][k], A[i+1][k]). * Bottom-up is essential: a top-down pass zeros A[i+1][k] with a rotation * that would later be undone when the next rotation (i+1, i+2) is formed * from an entry below, reviving A[i][k]. Each bottom-up rotation zeros the * bottom of the remaining nonzero pair and the entries below stay zero * (they are not mixed again, only rows i-1/i are mixed next). * * Already-tridiagonalized leading columns j < k are untouched: the mixed * rows are both >= k+1 > j+1, so A[i][j] and A[i+1][j] are both zero there. * The rotation on (i, i+1) also keeps column k+1..k+2 structure intact and * does not destroy earlier columns, so after column k is done the leading * (k+1)x(k+1) block is tridiagonal forever. * * On return: A is symmetric tridiagonal and A_orig = U A U^T (U = product * of every rotation applied, in application order, as U <- U G^T). */ template static void Tridiagonalize(Matrix &A, Matrix &U) { U = Matrix{0}; for (uint8_t i = 0; i < N; ++i) { U[i][i] = 1.0f; } float c = 0.0f, s = 0.0f; for (uint8_t k = 0; k + 2 < N; ++k) { for (int i = (int)N - 2; i >= (int)k + 1; --i) { GivensRotation(A.Get(i, k), A.Get(i + 1, k), c, s); ApplyRotationBothSides(A, (uint8_t)i, c, s); ApplyRotationToVectors(U, (uint8_t)i, c, s); } } } /** * See QR.hpp for the full contract. Implementation sketch: * * Phase 0 (N >= 3): Tridiagonalize(A, U) // A_orig = U A U^T * V = I. * while (hi > 0): * Deflate(A, 0, hi, tol); peel exact-zero trailing subdiagonals (hi--) * lo = top of the trailing unreduced block (scan down, stop at first * exact zero subdiagonal) * if lo == hi - 1: closed-form 2x2 eigen-solve; fold Vblock into V * else: one implicit Wilkinson-shifted QR step: * mu = WilkinsonShift(A[hi-1][hi-1], A[hi][hi-1], A[hi][hi]) * A[lo..hi diagonal] -= mu // whole block! * G1 = Givens(A[lo][lo], A[lo+1][lo]) * for i = lo..hi-1: * (i > lo: Gi = Givens(A[i][i], A[i+1][i])) * ApplyRotationBothSides(A, i, Gi) // A <- Gi A Gi^T * ApplyRotationToVectors(V, i, Gi) // V <- V Gi^T * A[lo..hi diagonal] += mu * eigenvalues = diag(A), sorted descending with matching V column swaps. * eigenvectors = U * V. * * Invariant maintained for N >= 3 (symmetric input): A is symmetric * tridiagonal (up to deflated zeros and ~1e-7 float roundoff in the * off-tridiagonal corners) at the top of every loop iteration, and * A_orig = U A U^T = (U V) A (U V)^T throughout (V = product of every * rotation applied so far, in application order, as V <- V Gi^T). At * convergence A = V D V^T and therefore A_orig = (U V) D (U V)^T. * * Orientation note: each chase rotation Gi is the ZEROING rotation * (Gi * (x, y)^T = (r, 0)^T). The step A <- Gi A Gi^T equals R Q with * R = Gi A upper-triangular (on the block) and Q = Gi^T -- i.e. it IS the * standard QR update Q(A - mu I)Q^T with Q the orthogonal QR factor. The * eigenvector accumulator therefore collects the Q factors: V <- V Gi^T. */ template void EigenQR(Matrix &matrixToDecompose, Matrix &eigenVectors, Matrix &eigenValues, uint32_t maxIterations, float tolerance) { static_assert(N >= 2, "QR::EigenQR requires N >= 2 (N = 1 is trivial)"); Matrix A = matrixToDecompose; // input is not modified Matrix V{0}; // NB: Matrix::Identity() is a static factory that returns by value; a // bare call would be a no-op. Set the diagonal explicitly. for (uint8_t i = 0; i < N; ++i) { V[i][i] = 1.0f; } // ------------------------------------------------------------------ // N == 2: closed-form solution (works for nonsymmetric input too) // ------------------------------------------------------------------ if (N == 2) { float l1 = 0.0f, l2 = 0.0f, c = 0.0f, s = 0.0f; Solve2x2Eigen(A, 0, l1, l2, c, s); // V = I * Vblock = [[c, -s], [s, c]] V[0][0] = c; V[0][1] = -s; V[1][0] = s; V[1][1] = c; eigenValues[0][0] = l1; eigenValues[1][0] = l2; for (uint8_t r = 0; r < N; ++r) for (uint8_t col = 0; col < N; ++col) eigenVectors[r][col] = V.Get(r, col); return; } // ------------------------------------------------------------------ // N >= 3: implicit shifted QR iteration (symmetric input required) // ------------------------------------------------------------------ // Phase 0: general symmetric -> symmetric tridiagonal. The implicit // QR bulge chase only preserves a tridiagonal structure, so the input // must be reduced first: A_orig = U A U^T with A tridiagonal. Matrix U{}; Tridiagonalize(A, U); uint32_t iter = 0; uint8_t hi = N - 1; while (hi > 0) { Deflate(A, 0, hi, tolerance); // Peel trailing rows whose subdiagonal is exactly zero (deflated or // already solved). Must be re-done every iteration: a peel is only // meaningful once the subdiagonal beneath it has converged. while (hi > 0 && A.Get(hi, hi - 1) == 0.0f) { --hi; } if (hi == 0) { break; // fully diagonal (within tolerance) } // Find the top of the trailing unreduced block: scan down from hi-1 // and stop at the first exact zero subdiagonal. A[hi][hi-1] != 0 here // (just peeled), so lo < hi. uint8_t lo = hi; for (int i = (int)hi - 1; i >= 0; --i) { if (A.Get(i + 1, i) == 0.0f) { break; } lo = (uint8_t)i; } if (lo + 1 == hi) { // Trailing unreduced block is 2x2: solve in closed form. float l1 = 0.0f, l2 = 0.0f, c = 0.0f, s = 0.0f; Solve2x2Eigen(A, lo, l1, l2, c, s); A[lo][lo] = l1; A[lo + 1][lo + 1] = l2; A[lo][lo + 1] = 0.0f; A[lo + 1][lo] = 0.0f; // Fold Vblock = [[c, -s], [s, c]] into V: V <- V * Vblock on // columns (lo, lo+1). NOTE the sign convention differs from // ApplyRotationToVectors (which applies [[c, s], [-s, c]]): // here column 0 of Vblock is (c, s)^T, column 1 is (-s, c)^T. for (uint8_t r = 0; r < N; ++r) { float x = V.Get(r, lo); float y = V.Get(r, lo + 1); V[r][lo] = c * x + s * y; V[r][lo + 1] = -s * x + c * y; } if (lo == 0) { break; // block reached the top: matrix is fully solved } hi = (uint8_t)(lo - 1); continue; } // One implicit Wilkinson-shifted QR step on block [lo, hi]. float mu = WilkinsonShift(A.Get(hi - 1, hi - 1), A.Get(hi, hi - 1), A.Get(hi, hi)); // The shift applies to the ENTIRE active block: bulge chasing // triangularizes (A - mu*I), and the first Givens rotation is formed // from (A[lo][lo] - mu, A[lo+1][lo]). for (uint8_t i = lo; i <= hi; ++i) { A[i][i] -= mu; } float c = 0.0f, s = 0.0f; for (uint8_t i = lo; i < hi; ++i) { if (i == lo) { GivensRotation(A.Get(lo, lo), A.Get(lo + 1, lo), c, s); } else { GivensRotation(A.Get(i, i), A.Get(i + 1, i), c, s); } ApplyRotationBothSides(A, i, c, s); ApplyRotationToVectors(V, i, c, s); } for (uint8_t i = lo; i <= hi; ++i) { A[i][i] += mu; } if (++iter >= maxIterations) { // Best-effort: fall through with the partially diagonalized A. break; } } // ------------------------------------------------------------------ // Collect eigenvalues and sort DESCENDING (swap eigenvectors to match) // ------------------------------------------------------------------ for (uint8_t i = 0; i < N; ++i) { eigenValues[i][0] = A.Get(i, i); } for (uint8_t i = 0; i < N - 1; ++i) { uint8_t k = i; for (uint8_t j = i + 1; j < N; ++j) { if (eigenValues.Get(j, 0) > eigenValues.Get(k, 0)) { k = j; } } if (k != i) { float t = eigenValues[i][0]; eigenValues[i][0] = eigenValues[k][0]; eigenValues[k][0] = t; for (uint8_t r = 0; r < N; ++r) { float x = V.Get(r, i); V[r][i] = V.Get(r, k); V[r][k] = x; } } } // True eigenvectors of the original matrix: U * V. Reuse the A buffer // (its diagonal has already been collected into eigenValues). U.Mult(V, A); for (uint8_t r = 0; r < N; ++r) { for (uint8_t col = 0; col < N; ++col) { eigenVectors[r][col] = A.Get(r, col); } } } } // namespace QR #endif // QR_H_