Add QR eigen library: implicit Wilkinson-shifted QR for symmetric matrices
Merge-Checker / build_and_test (pull_request) Failing after 28m10s

- src/QR.hpp / src/QR.cpp: fully templated QR::EigenQR (N >= 2), no heap
  allocation (3*N^2 float working buffers on stack). Givens tridiagonalization
  (bottom-up) + implicit Wilkinson-shifted QR with bulge chasing, relative
  deflation, exact-zero peeling, closed-form 2x2 termination.
- Matrix::EigenQR now delegates to QR::EigenQR (old unshifted body removed);
  eigenvalues sorted descending, eigenvectors in columns of the output.
- unit-tests/qr-build-blocks-tests.cpp: 8 building-block test cases
  (215 assertions) with scipy/numpy references.
- unit-tests/qr-reference-values.py: numpy/scipy reference generator mirroring
  every building block and the full pipeline (eigh, n=3..8).
- CMake: new 'qr' static library; Matrix links against it;
  qr-build-blocks-tests target enabled.
This commit is contained in:
2026-08-25 14:02:56 -04:00
parent ab0cea104c
commit c9a9492fcf
9 changed files with 1812 additions and 72 deletions
+19 -31
View File
@@ -5,6 +5,21 @@
#include "Matrix.hpp"
#endif
// Forward-declare QR::EigenQR so the Matrix::EigenQR implementation below can
// call it even when Matrix.cpp is pulled in through QR.hpp's own include chain
// (QR.cpp -> QR.hpp -> Matrix.hpp -> Matrix.cpp), where the QR namespace has
// not been declared yet at this point. If we are not already inside that
// chain, pull in the full QR library so its template definition is available.
namespace QR {
template <uint8_t N>
void EigenQR(Matrix<N, N> &matrixToDecompose, Matrix<N, N> &eigenVectors,
Matrix<N, 1> &eigenValues, uint32_t maxIterations,
float tolerance);
}
#ifndef QR_H_
#include "QR.hpp"
#endif
#ifdef MATRIX_H_ // since the .cpp file has to be included by the .hpp file this
// will evaluate to true
#include "Matrix.hpp"
@@ -571,37 +586,10 @@ void Matrix<rows, columns>::EigenQR(Matrix<rows, rows> &eigenVectors,
static_assert(rows > 1, "Matrix size must be > 1 for QR iteration");
static_assert(rows == columns, "Matrix size must be square for QR iteration");
Matrix<rows, rows> Ak = *this; // Copy original matrix
Matrix<rows, rows> QQ{Matrix<rows, rows>::Identity()};
Matrix<rows, rows> shift{0};
for (uint32_t iter = 0; iter < maxIterations; ++iter) {
Matrix<rows, rows> Q, R;
// // QR shift lets us "attack" the first diagonal to speed up the algorithm
// shift = Matrix<rows, rows>::Identity() * Ak[rows - 1][rows - 1];
(Ak - shift).QRDecomposition(Q, R);
Ak = R * Q + shift;
QQ = QQ * Q;
// Check convergence: off-diagonal norm
float offDiagSum = 0.0f;
for (uint32_t row = 1; row < rows; row++) {
for (uint32_t column = 0; column < row; column++) {
offDiagSum += fabs(Ak[row][column]);
}
}
if (offDiagSum < tolerance) {
break;
}
}
// Diagonal elements are the eigenvalues
for (uint8_t i = 0; i < rows; i++) {
eigenValues[i][0] = Ak[i][i];
}
eigenVectors = QQ;
// Delegate to the QR library: implicit shifted QR iteration with
// Wilkinson shift (see src/QR.hpp for the algorithm and conventions).
Matrix<rows, rows> A = *this; // QR::EigenQR does not modify its input
QR::EigenQR(A, eigenVectors, eigenValues, maxIterations, tolerance);
}
#endif // MATRIX_H_