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
+266 -17
View File
@@ -4,6 +4,7 @@
// include the module you're going to test next
#include "Matrix.hpp"
#include "QR.hpp"
#include "SVD.hpp"
// any other libraries
@@ -601,8 +602,78 @@ TEST_CASE("QR Decompositions", "Matrix") {
}
}
// ============================================================================
// Eigen QR Helpers (scipy references; eigenvector checks are sign-invariant)
// ============================================================================
/**
* @brief Normalized eigenpair residual ||A v - lambda v|| / (||A||_F + |lambda|)
*/
template <uint8_t N>
static float eigenResidual(const Matrix<N, N> &A, float lambda,
const Matrix<N, 1> &v) {
Matrix<N, 1> Av{};
A.Mult(v, Av);
float sum = 0.0f;
float frob = 0.0f;
for (uint8_t i = 0; i < N; i++) {
float d = Av.Get(i, 0) - lambda * v.Get(i, 0);
sum += d * d;
for (uint8_t j = 0; j < N; j++) {
float a = A.Get(i, j);
frob += a * a;
}
}
float scale = sqrtf(frob) + fabsf(lambda);
return sqrtf(sum) / scale;
}
/**
* @brief Column of the eigenvector matrix; used for the residual check.
*/
template <uint8_t N>
static Matrix<N, 1> eigenColumn(const Matrix<N, N> &V, uint8_t col) {
Matrix<N, 1> v{};
for (uint8_t i = 0; i < N; i++) {
v[i][0] = V.Get(i, col);
}
return v;
}
/**
* @brief Check V^T V ~ I (eigenvectors orthonormal).
*/
template <uint8_t N>
static bool isOrthogonal(const Matrix<N, N> &V, float tol = 1e-4f) {
Matrix<N, N> Vt = V.Transpose();
Matrix<N, N> VtV{};
Vt.Mult(V, VtV);
for (uint8_t i = 0; i < N; i++) {
for (uint8_t j = 0; j < N; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
if (fabsf(VtV.Get(i, j) - expected) > tol) {
return false;
}
}
}
return true;
}
/**
* @brief Sign-invariant component check: |actual| within max(1e-4, 1e-3*|ref|)
* of ref (ref is the ABSOLUTE value from the scipy reference).
*/
static bool componentMatches(float actual, float refAbs) {
float a = fabsf(actual);
float tol = 1e-4f;
if (refAbs * 1e-3f > tol) {
tol = refAbs * 1e-3f;
}
return fabsf(a - refAbs) <= tol;
}
TEST_CASE("Eigenvalues and Vectors", "Matrix") {
SECTION("2x2 Eigen") {
SECTION("2x2 Eigen (nonsymmetric, closed form)") {
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
Matrix<2, 2> vectors{};
Matrix<2, 1> values{};
@@ -615,28 +686,206 @@ TEST_CASE("Eigenvalues and Vectors", "Matrix") {
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(-0.372281f, 1e-4f));
}
SECTION("3x3 Rank Defficient Eigen") {
SKIP("Skipping this because QR decomposition isn't ready for it");
// this symmetrix tridiagonal matrix is well behaved for testing
Matrix<3, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
// Reference values: numpy.linalg.eigh on float32 matrices.
// Eigenvector component references are ABSOLUTE values (signs arbitrary).
SECTION("3x3 Symmetric Eigen") {
Matrix<3, 3> A{1, 2, 3, 2, 5, 8, 3, 8, 9};
Matrix<3, 3> vectors{};
Matrix<3, 1> values{};
A.EigenQR(vectors, values, 1000000, 1e-8f);
A.EigenQR(vectors, values, 10000, 1e-6f);
std::string strBuf1 = "";
vectors.ToString(strBuf1);
std::cout << "Vectors:\n" << strBuf1 << std::endl;
strBuf1 = "";
values.ToString(strBuf1);
std::cout << "Values:\n" << strBuf1 << std::endl;
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(16.102417f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(0.191920f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(-1.2943381f, 1e-4f));
REQUIRE_THAT(vectors[0][0], Catch::Matchers::WithinRel(0.23197f, 1e-4f));
REQUIRE_THAT(vectors[1][0], Catch::Matchers::WithinRel(0.525322f, 1e-4f));
REQUIRE_THAT(vectors[2][0], Catch::Matchers::WithinRel(0.81867f, 1e-4f));
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(-1.11684f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.231657207f));
REQUIRE(componentMatches(vectors[1][0], 0.59582746f));
REQUIRE(componentMatches(vectors[2][0], 0.768976331f));
REQUIRE(componentMatches(vectors[0][1], 0.956842422f));
REQUIRE(componentMatches(vectors[1][1], 0.282139271f));
REQUIRE(componentMatches(vectors[2][1], 0.0696421042f));
REQUIRE(componentMatches(vectors[0][2], 0.175463736f));
REQUIRE(componentMatches(vectors[1][2], 0.75192225f));
REQUIRE(componentMatches(vectors[2][2], 0.635472536f));
// eigenvectors orthonormal; eigenpair residuals small
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 3; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("3x3 Rank Deficient Eigen") {
// A = v v^T with v = [1, 2, 3]: eigenvalues {14, 0, 0}
Matrix<3, 3> A{1, 2, 3, 2, 4, 6, 3, 6, 9};
Matrix<3, 3> vectors{};
Matrix<3, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(14.0f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(16.1168f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// dominant eigenvector is v/|v| (sign-invariant); the two null-space
// eigenvectors may be ANY orthonormal basis of the null plane, so only
// orthogonality + residuals are checked for the full matrix.
REQUIRE(componentMatches(vectors[0][0], 0.267261237f));
REQUIRE(componentMatches(vectors[1][0], 0.534522474f));
REQUIRE(componentMatches(vectors[2][0], 0.801783741f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 3; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("4x4 Symmetric Eigen") {
Matrix<4, 4> A{2, 1, 0, 1, 1, 3, 1, 0, 0, 1, 4, 1, 1, 0, 1, 5};
Matrix<4, 4> vectors{};
Matrix<4, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues are exactly {6, 4, 3, 1}
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(6.0f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(4.0f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(3.0f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(1.0f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.258198887f));
REQUIRE(componentMatches(vectors[1][0], 0.258198887f));
REQUIRE(componentMatches(vectors[2][0], 0.516397774f));
REQUIRE(componentMatches(vectors[3][0], 0.774596691f));
REQUIRE(componentMatches(vectors[0][1], 0.0f));
REQUIRE(componentMatches(vectors[1][1], 0.577350259f));
REQUIRE(componentMatches(vectors[2][1], 0.577350259f));
REQUIRE(componentMatches(vectors[3][1], 0.577350259f));
REQUIRE(componentMatches(vectors[0][2], 0.577350259f));
REQUIRE(componentMatches(vectors[1][2], 0.577350259f));
REQUIRE(componentMatches(vectors[2][2], 0.577350259f));
REQUIRE(componentMatches(vectors[3][2], 0.0f));
REQUIRE(componentMatches(vectors[0][3], 0.774596691f));
REQUIRE(componentMatches(vectors[1][3], 0.516397774f));
REQUIRE(componentMatches(vectors[2][3], 0.258198887f));
REQUIRE(componentMatches(vectors[3][3], 0.258198887f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 4; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("5x5 Symmetric Eigen") {
Matrix<5, 5> A{3, 1, 0, 0, 1, 1, 4, 1, 0, 0, 0, 1, 5, 1, 0, 0, 0, 1, 6, 1,
1, 0, 0, 1, 7};
Matrix<5, 5> vectors{};
Matrix<5, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(7.90154457f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(6.20044184f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(5.14503145f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(3.61823463f, 1e-4f));
REQUIRE_THAT(values[4][0], Catch::Matchers::WithinRel(2.13474774f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.182430908f));
REQUIRE(componentMatches(vectors[1][0], 0.102749094f));
REQUIRE(componentMatches(vectors[2][0], 0.21844925f));
REQUIRE(componentMatches(vectors[3][0], 0.531091094f));
REQUIRE(componentMatches(vectors[4][0], 0.791444063f));
REQUIRE(componentMatches(vectors[0][1], 0.0877681747f));
REQUIRE(componentMatches(vectors[1][1], 0.245861098f));
REQUIRE(componentMatches(vectors[2][1], 0.628771126f));
REQUIRE(componentMatches(vectors[3][1], 0.508941948f));
REQUIRE(componentMatches(vectors[4][1], 0.526758015f));
REQUIRE(componentMatches(vectors[0][2], 0.349721253f));
REQUIRE(componentMatches(vectors[1][2], 0.628706098f));
REQUIRE(componentMatches(vectors[2][2], 0.370167077f));
REQUIRE(componentMatches(vectors[3][2], 0.575020194f));
REQUIRE(componentMatches(vectors[4][2], 0.121457018f));
REQUIRE(componentMatches(vectors[0][3], 0.429638386f));
REQUIRE(componentMatches(vectors[1][3], 0.498553723f));
REQUIRE(componentMatches(vectors[2][3], 0.619968951f));
REQUIRE(componentMatches(vectors[3][3], 0.35809797f));
REQUIRE(componentMatches(vectors[4][3], 0.232936427f));
REQUIRE(componentMatches(vectors[0][4], 0.807540476f));
REQUIRE(componentMatches(vectors[1][4], 0.534011006f));
REQUIRE(componentMatches(vectors[2][4], 0.188524753f));
REQUIRE(componentMatches(vectors[3][4], 0.00615991838f));
REQUIRE(componentMatches(vectors[4][4], 0.164715111f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 5; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("6x6 Symmetric Eigen") {
Matrix<6, 6> A{4, 1, 0, 0, 0, 1, 1, 5, 1, 0, 0, 0, 0, 1, 6, 1, 0, 0, 0, 0,
1, 7, 1, 0, 0, 0, 0, 1, 8, 1, 1, 0, 0, 0, 1, 3};
Matrix<6, 6> vectors{};
Matrix<6, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(8.86080551f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(7.25410175f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(6.11490774f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(4.88509226f, 1e-4f));
REQUIRE_THAT(values[4][0], Catch::Matchers::WithinRel(3.74589825f, 1e-4f));
REQUIRE_THAT(values[5][0], Catch::Matchers::WithinRel(2.13919425f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.0430923924f));
REQUIRE(componentMatches(vectors[1][0], 0.0662503168f));
REQUIRE(componentMatches(vectors[2][0], 0.212687209f));
REQUIRE(componentMatches(vectors[3][0], 0.542206466f));
REQUIRE(componentMatches(vectors[4][0], 0.7962538f));
REQUIRE(componentMatches(vectors[5][0], 0.143213451f));
REQUIRE(componentMatches(vectors[0][1], 0.0623276457f));
REQUIRE(componentMatches(vectors[1][1], 0.307613879f));
REQUIRE(componentMatches(vectors[2][1], 0.631065309f));
REQUIRE(componentMatches(vectors[3][1], 0.483806193f));
REQUIRE(componentMatches(vectors[4][1], 0.508129358f));
REQUIRE(componentMatches(vectors[5][1], 0.104793385f));
REQUIRE(componentMatches(vectors[0][2], 0.374228716f));
REQUIRE(componentMatches(vectors[1][2], 0.605694294f));
REQUIRE(componentMatches(vectors[2][2], 0.301064402f));
REQUIRE(componentMatches(vectors[3][2], 0.571099699f));
REQUIRE(componentMatches(vectors[4][2], 0.204411641f));
REQUIRE(componentMatches(vectors[5][2], 0.185764849f));
REQUIRE(componentMatches(vectors[0][3], 0.571099699f));
REQUIRE(componentMatches(vectors[1][3], 0.301064402f));
REQUIRE(componentMatches(vectors[2][3], 0.605694294f));
REQUIRE(componentMatches(vectors[3][3], 0.374228716f));
REQUIRE(componentMatches(vectors[4][3], 0.185764849f));
REQUIRE(componentMatches(vectors[5][3], 0.204411641f));
REQUIRE(componentMatches(vectors[0][4], 0.483806193f));
REQUIRE(componentMatches(vectors[1][4], 0.631065309f));
REQUIRE(componentMatches(vectors[2][4], 0.307613879f));
REQUIRE(componentMatches(vectors[3][4], 0.0623276457f));
REQUIRE(componentMatches(vectors[4][4], 0.104793385f));
REQUIRE(componentMatches(vectors[5][4], 0.508129358f));
REQUIRE(componentMatches(vectors[0][5], 0.542206466f));
REQUIRE(componentMatches(vectors[1][5], 0.212687209f));
REQUIRE(componentMatches(vectors[2][5], 0.0662503168f));
REQUIRE(componentMatches(vectors[3][5], 0.0430923924f));
REQUIRE(componentMatches(vectors[4][5], 0.143213451f));
REQUIRE(componentMatches(vectors[5][5], 0.7962538f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 6; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
}