Add QR eigen library: implicit Wilkinson-shifted QR for symmetric matrices
Merge-Checker / build_and_test (pull_request) Failing after 28m10s
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:
+266
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user