Fixes for control systems AND SVD and QR decomposition (#9)
Reviewed-on: #9 Co-authored-by: Cynopolis <quinn.henthorne@gmail.com>
This commit was merged in pull request #9.
This commit is contained in:
@@ -13,6 +13,7 @@ add_executable(matrix-tests matrix-tests.cpp)
|
||||
target_link_libraries(matrix-tests
|
||||
PRIVATE
|
||||
matrix
|
||||
qr
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
@@ -32,4 +33,34 @@ target_link_libraries(vector-3d-tests
|
||||
PRIVATE
|
||||
vector-3d
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
# SVD building block tests
|
||||
add_executable(svd-build-blocks-tests svd-build-blocks-tests.cpp)
|
||||
|
||||
target_link_libraries(svd-build-blocks-tests
|
||||
PRIVATE
|
||||
matrix
|
||||
svd
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
# SVD integration tests
|
||||
add_executable(svd-integration-test svd-integration-test.cpp)
|
||||
|
||||
target_link_libraries(svd-integration-test
|
||||
PRIVATE
|
||||
matrix
|
||||
svd
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
|
||||
# QR building block tests
|
||||
add_executable(qr-build-blocks-tests qr-build-blocks-tests.cpp)
|
||||
|
||||
target_link_libraries(qr-build-blocks-tests
|
||||
PRIVATE
|
||||
matrix
|
||||
qr
|
||||
Catch2::Catch2WithMain
|
||||
)
|
||||
+932
-23
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,8 @@ TEST_CASE("Timing Tests", "Matrix") {
|
||||
|
||||
SECTION("Determinant") {
|
||||
for (uint32_t i{0}; i < 1000000; i++) {
|
||||
float det1 = mat4.Det();
|
||||
float det = mat4.Det();
|
||||
(void)det;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
// include the unit test framework first
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
// include the module you're going to test next
|
||||
#include "Matrix.hpp"
|
||||
#include "QR.hpp"
|
||||
|
||||
// any other libraries
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Frobenius norm of an N x N matrix.
|
||||
*/
|
||||
template <uint8_t N>
|
||||
static float frob(const Matrix<N, N> &M) {
|
||||
float sum = 0.0f;
|
||||
for (uint8_t i = 0; i < N; i++)
|
||||
for (uint8_t j = 0; j < N; j++) {
|
||||
float v = M.Get(i, j);
|
||||
sum += v * v;
|
||||
}
|
||||
return sqrtf(sum);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check M is orthogonal (M^T M ~ I).
|
||||
*/
|
||||
template <uint8_t N>
|
||||
static bool isOrthogonal(const Matrix<N, N> &M, float tol = 1e-5f) {
|
||||
Matrix<N, N> Mt = M.Transpose();
|
||||
Matrix<N, N> MtM{};
|
||||
Mt.Mult(M, MtM);
|
||||
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(MtM.Get(i, j) - expected) > tol)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 3x3 trace.
|
||||
*/
|
||||
static float trace3(const Matrix<3, 3> &A) {
|
||||
return A.Get(0, 0) + A.Get(1, 1) + A.Get(2, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 3x3 sum of principal 2x2 minors (2nd elementary invariant).
|
||||
*/
|
||||
static float e2_3x3(const Matrix<3, 3> &A) {
|
||||
return A.Get(0, 0) * A.Get(1, 1) - A.Get(0, 1) * A.Get(0, 1) +
|
||||
A.Get(0, 0) * A.Get(2, 2) - A.Get(0, 2) * A.Get(0, 2) +
|
||||
A.Get(1, 1) * A.Get(2, 2) - A.Get(1, 2) * A.Get(1, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 3x3 determinant.
|
||||
*/
|
||||
static float det3(const Matrix<3, 3> &A) {
|
||||
return A.Get(0, 0) *
|
||||
(A.Get(1, 1) * A.Get(2, 2) - A.Get(1, 2) * A.Get(2, 1)) -
|
||||
A.Get(0, 1) *
|
||||
(A.Get(1, 0) * A.Get(2, 2) - A.Get(1, 2) * A.Get(2, 0)) +
|
||||
A.Get(0, 2) *
|
||||
(A.Get(1, 0) * A.Get(2, 1) - A.Get(1, 1) * A.Get(2, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sign-invariant comparison of |actual| against refAbs.
|
||||
*/
|
||||
static bool matchesAbs(float actual, float refAbs, float relTol = 1e-5f,
|
||||
float absTol = 1e-6f) {
|
||||
float a = fabsf(actual);
|
||||
if (refAbs < 1e-3f)
|
||||
return a < absTol + relTol;
|
||||
return fabsf(a - refAbs) <= relTol * refAbs;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 1: GivensRotation
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: GivensRotation", "[Matrix][QR]") {
|
||||
// R = [[c, s], [-s, c]] must satisfy R * (a, b)^T = (r, 0)^T.
|
||||
|
||||
{
|
||||
// Reference: hypot(2, 1) = sqrt(5) = 2.236067977
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(2.0f, 1.0f, c, s);
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(0.894427191f, 1e-6f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(0.447213595f, 1e-6f));
|
||||
REQUIRE_THAT(c * 2.0f + s * 1.0f,
|
||||
Catch::Matchers::WithinRel(2.236067977f, 1e-6f));
|
||||
REQUIRE_THAT(-s * 2.0f + c * 1.0f, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
}
|
||||
|
||||
{
|
||||
// Reference: hypot(3, 4) = 5 exactly
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(3.0f, 4.0f, c, s);
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(0.6f, 1e-6f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(0.8f, 1e-6f));
|
||||
REQUIRE_THAT(c * 3.0f + s * 4.0f, Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||||
REQUIRE_THAT(-s * 3.0f + c * 4.0f, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
}
|
||||
|
||||
{
|
||||
// Pure second component: c = 0, s = 1
|
||||
float c = 1, s = 1;
|
||||
QR::GivensRotation(0.0f, 5.0f, c, s);
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
}
|
||||
|
||||
{
|
||||
// Zero vector: identity rotation
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(0.0f, 0.0f, c, s);
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-7f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
}
|
||||
|
||||
{
|
||||
// Negative first component preserves the sign of c
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(-2.0f, 1.0f, c, s);
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(-0.894427191f, 1e-6f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(0.447213595f, 1e-6f));
|
||||
REQUIRE_THAT(-s * -2.0f + c * 1.0f, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 2: ApplyRotationBothSides (similarity A <- G A G^T)
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: ApplyRotationBothSides", "[Matrix][QR]") {
|
||||
// Reference (numpy, float64): A = [[2,1,0],[1,3,1],[0,1,4]], i = 0,
|
||||
// Givens(2,1) -> G A G^T =
|
||||
// [[ 3.0, 1.0, 0.447213595],
|
||||
// [ 1.0, 2.0, 0.894427191],
|
||||
// [ 0.447213595, 0.894427191, 4.0]]
|
||||
// (Note: G A G^T with G zeroing (2,1) sends the A[0][1] coupling into the
|
||||
// (0,2) corner, NOT into the subdiagonal -- the subdiagonal-zeroing happens
|
||||
// in the QR chase context where the bulge column has the right shape.)
|
||||
{
|
||||
Matrix<3, 3> A{2, 1, 0, 1, 3, 1, 0, 1, 4};
|
||||
float c = 0.894427191f, s = 0.447213595f;
|
||||
|
||||
QR::ApplyRotationBothSides(A, 0, c, s);
|
||||
|
||||
REQUIRE_THAT(A.Get(0, 0), Catch::Matchers::WithinRel(3.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(0, 1), Catch::Matchers::WithinRel(1.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(0, 2),
|
||||
Catch::Matchers::WithinRel(0.447213595f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(1, 1), Catch::Matchers::WithinRel(2.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(1, 2),
|
||||
Catch::Matchers::WithinRel(0.894427191f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(2, 2), Catch::Matchers::WithinRel(4.0f, 1e-5f));
|
||||
|
||||
// Symmetry must be preserved exactly in both triangles
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
for (uint8_t j = 0; j < 3; j++)
|
||||
REQUIRE(A.Get(i, j) == A.Get(j, i));
|
||||
}
|
||||
|
||||
// Same check at i = 1.
|
||||
// Reference (numpy, float64): B = [[5,0,1],[0,6,2],[1,2,7]], i = 1,
|
||||
// Givens(6,2) -> G B G^T =
|
||||
// [[ 5.0, 0.316227766, 0.948683298],
|
||||
// [ 0.316227766, 7.3, 1.9],
|
||||
// [ 0.948683298, 1.9, 5.7]]
|
||||
{
|
||||
Matrix<3, 3> B{5, 0, 1, 0, 6, 2, 1, 2, 7};
|
||||
float c = 0.948683298f, s = 0.316227766f;
|
||||
|
||||
QR::ApplyRotationBothSides(B, 1, c, s);
|
||||
|
||||
REQUIRE_THAT(B.Get(0, 0), Catch::Matchers::WithinRel(5.0f, 1e-5f));
|
||||
REQUIRE_THAT(B.Get(0, 1),
|
||||
Catch::Matchers::WithinRel(0.316227766f, 1e-5f));
|
||||
REQUIRE_THAT(B.Get(0, 2),
|
||||
Catch::Matchers::WithinRel(0.948683298f, 1e-5f));
|
||||
REQUIRE_THAT(B.Get(1, 1), Catch::Matchers::WithinRel(7.3f, 1e-5f));
|
||||
REQUIRE_THAT(B.Get(1, 2), Catch::Matchers::WithinRel(1.9f, 1e-5f));
|
||||
REQUIRE_THAT(B.Get(2, 2), Catch::Matchers::WithinRel(5.7f, 1e-5f));
|
||||
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
for (uint8_t j = 0; j < 3; j++)
|
||||
REQUIRE(B.Get(i, j) == B.Get(j, i));
|
||||
}
|
||||
|
||||
// Identity rotation leaves the matrix unchanged
|
||||
{
|
||||
Matrix<3, 3> C{1, 2, 3, 2, 4, 5, 3, 5, 6};
|
||||
QR::ApplyRotationBothSides(C, 1, 1.0f, 0.0f);
|
||||
REQUIRE(C.Get(0, 0) == 1.0f);
|
||||
REQUIRE(C.Get(0, 1) == 2.0f);
|
||||
REQUIRE(C.Get(0, 2) == 3.0f);
|
||||
REQUIRE(C.Get(1, 1) == 4.0f);
|
||||
REQUIRE(C.Get(1, 2) == 5.0f);
|
||||
REQUIRE(C.Get(2, 2) == 6.0f);
|
||||
}
|
||||
|
||||
// Spectrum invariants (trace, Frobenius norm) are preserved. (c, s)
|
||||
// must be a unit vector for G A G^T to be a similarity transform.
|
||||
{
|
||||
Matrix<3, 3> D{1, 2, 3, 2, 5, 8, 3, 8, 9};
|
||||
float tr = trace3(D);
|
||||
float fn = frob(D);
|
||||
float c = 0.6f, s = 0.8f;
|
||||
QR::ApplyRotationBothSides(D, 0, c, s);
|
||||
REQUIRE_THAT(trace3(D), Catch::Matchers::WithinRel(tr, 1e-5f));
|
||||
REQUIRE_THAT(frob(D), Catch::Matchers::WithinRel(fn, 1e-5f));
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// TEST 3: ApplyRotationToVectors (V <- V G^T)
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: ApplyRotationToVectors", "[Matrix][QR]") {
|
||||
// V = I, i = 0, Givens(2,1): V <- I * G^T with G^T = [[c, -s], [s, c]] =
|
||||
// [[ c, -s, 0],
|
||||
// [ s, c, 0],
|
||||
// [ 0, 0, 1]]
|
||||
{
|
||||
Matrix<3, 3> V{0};
|
||||
V[0][0] = 1;
|
||||
V[1][1] = 1;
|
||||
V[2][2] = 1;
|
||||
float c = 0.894427191f, s = 0.447213595f;
|
||||
|
||||
QR::ApplyRotationToVectors(V, 0, c, s);
|
||||
|
||||
REQUIRE_THAT(V.Get(0, 0), Catch::Matchers::WithinRel(0.894427191f, 1e-6f));
|
||||
REQUIRE_THAT(V.Get(0, 1), Catch::Matchers::WithinRel(-0.447213595f, 1e-6f));
|
||||
REQUIRE_THAT(V.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(V.Get(1, 0), Catch::Matchers::WithinRel(0.447213595f, 1e-6f));
|
||||
REQUIRE_THAT(V.Get(1, 1), Catch::Matchers::WithinRel(0.894427191f, 1e-6f));
|
||||
REQUIRE_THAT(V.Get(1, 2), Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(V.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(V.Get(2, 1), Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(V.Get(2, 2), Catch::Matchers::WithinRel(1.0f, 1e-7f));
|
||||
|
||||
// Product of rotations must stay orthogonal
|
||||
REQUIRE(isOrthogonal(V));
|
||||
}
|
||||
|
||||
// Two successive rotations accumulate (V <- V G1^T G2^T)
|
||||
// Reference (numpy, float64):
|
||||
// [[ 0.894427191, -0.424264069, 0.141421356],
|
||||
// [ 0.447213595, 0.848528137, -0.282842712],
|
||||
// [ 0.0, 0.316227766, 0.948683298]]
|
||||
{
|
||||
Matrix<3, 3> V{0};
|
||||
V[0][0] = 1;
|
||||
V[1][1] = 1;
|
||||
V[2][2] = 1;
|
||||
QR::ApplyRotationToVectors(V, 0, 0.894427191f, 0.447213595f);
|
||||
QR::ApplyRotationToVectors(V, 1, 0.948683298f, 0.316227766f);
|
||||
REQUIRE(isOrthogonal(V));
|
||||
// Column 0 was only touched by the first rotation
|
||||
REQUIRE_THAT(V.Get(0, 0), Catch::Matchers::WithinRel(0.894427191f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(1, 0), Catch::Matchers::WithinRel(0.447213595f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(V.Get(0, 1), Catch::Matchers::WithinRel(-0.424264069f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(0, 2), Catch::Matchers::WithinRel(0.141421356f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(1, 2), Catch::Matchers::WithinRel(-0.282842712f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(2, 1), Catch::Matchers::WithinRel(0.316227766f, 1e-5f));
|
||||
REQUIRE_THAT(V.Get(2, 2), Catch::Matchers::WithinRel(0.948683298f, 1e-5f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 4: WilkinsonShift
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: WilkinsonShift", "[Matrix][QR]") {
|
||||
// mu = (a+d)/2 - sign(a-d) * sqrt(((a-d)/2)^2 + b^2)
|
||||
// Reference: eigenvalues of [[2,1],[1,4]] are 1.5858, 4.4142; closest
|
||||
// to d = 4 is 4.414213562.
|
||||
REQUIRE_THAT(QR::WilkinsonShift(2.0f, 1.0f, 4.0f),
|
||||
Catch::Matchers::WithinRel(4.414213562f, 1e-6f));
|
||||
|
||||
// [[5,2],[2,1]]: eigenvalues 0.1716, 5.8284; closest to d = 1 is 0.171572875
|
||||
REQUIRE_THAT(QR::WilkinsonShift(5.0f, 2.0f, 1.0f),
|
||||
Catch::Matchers::WithinRel(0.171572875f, 1e-5f));
|
||||
|
||||
// Zero off-diagonal: returns d itself (sign(0) = +1 picks d, not a)
|
||||
REQUIRE_THAT(QR::WilkinsonShift(3.0f, 0.0f, 7.0f),
|
||||
Catch::Matchers::WithinRel(7.0f, 1e-7f));
|
||||
REQUIRE_THAT(QR::WilkinsonShift(7.0f, 0.0f, 3.0f),
|
||||
Catch::Matchers::WithinRel(3.0f, 1e-7f));
|
||||
|
||||
// a == d: shift is the larger-magnitude off-diagonal combination
|
||||
// [[1,3],[3,1]]: eigenvalues -2, 4; closest to d = 1 is -2
|
||||
REQUIRE_THAT(QR::WilkinsonShift(1.0f, 3.0f, 1.0f),
|
||||
Catch::Matchers::WithinRel(-2.0f, 1e-6f));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 5: Solve2x2Eigen
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: Solve2x2Eigen", "[Matrix][QR]") {
|
||||
// Symmetric block [[2,1],[1,3]]:
|
||||
// eigenvalues 1.381966011, 3.618033989;
|
||||
// eigenvector of 3.618033989 is +/- (0.525731112, 0.850650808)
|
||||
{
|
||||
Matrix<2, 2> A{2, 1, 1, 3};
|
||||
float lHi = 0, lLo = 0, c = 0, s = 0;
|
||||
QR::Solve2x2Eigen(A, 0, lHi, lLo, c, s);
|
||||
|
||||
REQUIRE_THAT(lHi, Catch::Matchers::WithinRel(3.618033989f, 1e-6f));
|
||||
REQUIRE_THAT(lLo, Catch::Matchers::WithinRel(1.381966011f, 1e-6f));
|
||||
REQUIRE(matchesAbs(c, 0.525731112f));
|
||||
REQUIRE(matchesAbs(s, 0.850650808f));
|
||||
|
||||
// Residual: A * vHi = lHi * vHi with vHi = (c, s)
|
||||
REQUIRE_THAT(c * 2.0f + s * 1.0f,
|
||||
Catch::Matchers::WithinRel(lHi * c, 1e-5f));
|
||||
REQUIRE_THAT(c * 1.0f + s * 3.0f,
|
||||
Catch::Matchers::WithinRel(lHi * s, 1e-5f));
|
||||
// Second eigenvector vLo = (-s, c)
|
||||
REQUIRE_THAT(-s * 2.0f + c * 1.0f,
|
||||
Catch::Matchers::WithinRel(lLo * -s, 1e-5f));
|
||||
REQUIRE_THAT(-s * 1.0f + c * 3.0f,
|
||||
Catch::Matchers::WithinRel(lLo * c, 1e-5f));
|
||||
}
|
||||
|
||||
// Nonsymmetric block [[1,2],[3,4]] (used by the N == 2 entry point):
|
||||
// eigenvalues 5.372281323, -0.372281323;
|
||||
// eigenvector of 5.372281323 is +/- (0.415973558, 0.909376709)
|
||||
{
|
||||
Matrix<2, 2> A{1, 2, 3, 4};
|
||||
float lHi = 0, lLo = 0, c = 0, s = 0;
|
||||
QR::Solve2x2Eigen(A, 0, lHi, lLo, c, s);
|
||||
|
||||
REQUIRE_THAT(lHi, Catch::Matchers::WithinRel(5.372281323f, 1e-6f));
|
||||
REQUIRE_THAT(lLo, Catch::Matchers::WithinRel(-0.372281323f, 1e-6f));
|
||||
REQUIRE(matchesAbs(c, 0.415973558f));
|
||||
REQUIRE(matchesAbs(s, 0.909376709f));
|
||||
|
||||
// Both-row residual with vHi = (c, s): A v = l v
|
||||
REQUIRE_THAT(c * 1.0f + s * 2.0f,
|
||||
Catch::Matchers::WithinRel(lHi * c, 1e-5f));
|
||||
REQUIRE_THAT(c * 3.0f + s * 4.0f,
|
||||
Catch::Matchers::WithinRel(lHi * s, 1e-5f));
|
||||
}
|
||||
|
||||
// Diagonal blocks: eigenvectors are coordinate vectors
|
||||
{
|
||||
Matrix<2, 2> A{5, 0, 0, 2};
|
||||
float lHi = 0, lLo = 0, c = 0, s = 0;
|
||||
QR::Solve2x2Eigen(A, 0, lHi, lLo, c, s);
|
||||
REQUIRE_THAT(lHi, Catch::Matchers::WithinRel(5.0f, 1e-7f));
|
||||
REQUIRE_THAT(lLo, Catch::Matchers::WithinRel(2.0f, 1e-7f));
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-7f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
|
||||
A = Matrix<2, 2>{2, 0, 0, 5};
|
||||
QR::Solve2x2Eigen(A, 0, lHi, lLo, c, s);
|
||||
REQUIRE_THAT(lHi, Catch::Matchers::WithinRel(5.0f, 1e-7f));
|
||||
REQUIRE_THAT(lLo, Catch::Matchers::WithinRel(2.0f, 1e-7f));
|
||||
REQUIRE_THAT(c, Catch::Matchers::WithinAbs(0.0f, 1e-7f));
|
||||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(1.0f, 1e-7f));
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// TEST 6: Deflate
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: Deflate", "[Matrix][QR]") {
|
||||
// subdiag[0] = 1e-9 <= 1e-6 * (|2| + |3|) = 5e-6 -> deflated
|
||||
// subdiag[1] = 0.5 > 1e-6 * (|3| + |4|) = 7e-6 -> kept
|
||||
{
|
||||
Matrix<3, 3> A{2, 1e-9f, 0, 1e-9f, 3, 0.5f, 0, 0.5f, 4};
|
||||
QR::Deflate(A, 0, 2, 1e-6f);
|
||||
|
||||
REQUIRE(A.Get(1, 0) == 0.0f);
|
||||
REQUIRE(A.Get(0, 1) == 0.0f);
|
||||
REQUIRE_THAT(A.Get(2, 1), Catch::Matchers::WithinRel(0.5f, 1e-7f));
|
||||
REQUIRE_THAT(A.Get(1, 2), Catch::Matchers::WithinRel(0.5f, 1e-7f));
|
||||
// Diagonals untouched
|
||||
REQUIRE_THAT(A.Get(0, 0), Catch::Matchers::WithinRel(2.0f, 1e-7f));
|
||||
REQUIRE_THAT(A.Get(1, 1), Catch::Matchers::WithinRel(3.0f, 1e-7f));
|
||||
REQUIRE_THAT(A.Get(2, 2), Catch::Matchers::WithinRel(4.0f, 1e-7f));
|
||||
}
|
||||
|
||||
// Nothing deflated when all subdiagonals are well above tolerance
|
||||
{
|
||||
Matrix<3, 3> A{2, 0.1f, 0, 0.1f, 3, 0.2f, 0, 0.2f, 4};
|
||||
QR::Deflate(A, 0, 2, 1e-6f);
|
||||
REQUIRE_THAT(A.Get(1, 0), Catch::Matchers::WithinRel(0.1f, 1e-7f));
|
||||
REQUIRE_THAT(A.Get(2, 1), Catch::Matchers::WithinRel(0.2f, 1e-7f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 7: Tridiagonalize
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: Tridiagonalize", "[Matrix][QR]") {
|
||||
// 4x4 symmetric with a full (0,3) corner coupling
|
||||
{
|
||||
Matrix<4, 4> A{2, 1, 0, 1, 1, 3, 1, 0, 0, 1, 4, 1, 1, 0, 1, 5};
|
||||
Matrix<4, 4> Aorig = A;
|
||||
Matrix<4, 4> U{0};
|
||||
|
||||
QR::Tridiagonalize(A, U);
|
||||
|
||||
// Off-tridiagonal entries must be zero up to float32 roundoff (the
|
||||
// Givens zeroing cancels only in exact arithmetic; residuals are
|
||||
// ~1e-7 for O(1) entries).
|
||||
REQUIRE_THAT(A.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(0, 3), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(1, 3), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(A.Get(3, 1), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
|
||||
// Symmetry preserved exactly
|
||||
for (uint8_t i = 0; i < 4; i++)
|
||||
for (uint8_t j = 0; j < 4; j++)
|
||||
REQUIRE(A.Get(i, j) == A.Get(j, i));
|
||||
|
||||
// U must be orthogonal
|
||||
REQUIRE(isOrthogonal(U));
|
||||
|
||||
// Reconstruction: U * A_tri * U^T == Aorig (absolute check for
|
||||
// originally-zero entries: WithinRel has no absolute fallback there)
|
||||
Matrix<4, 4> UAt{};
|
||||
U.Mult(A, UAt);
|
||||
Matrix<4, 4> UAtU{};
|
||||
UAt.Mult(U.Transpose(), UAtU);
|
||||
for (uint8_t i = 0; i < 4; i++)
|
||||
for (uint8_t j = 0; j < 4; j++) {
|
||||
float actual = UAtU.Get(i, j);
|
||||
float expected = Aorig.Get(i, j);
|
||||
if (fabsf(expected) < 1e-3f)
|
||||
REQUIRE_THAT(actual, Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
else
|
||||
REQUIRE_THAT(actual,
|
||||
Catch::Matchers::WithinRel(expected, 1e-5f));
|
||||
}
|
||||
|
||||
// Spectrum invariants match the original
|
||||
{
|
||||
float tr0 = Aorig.Get(0, 0) + Aorig.Get(1, 1) + Aorig.Get(2, 2) +
|
||||
Aorig.Get(3, 3);
|
||||
float tr1 = A.Get(0, 0) + A.Get(1, 1) + A.Get(2, 2) + A.Get(3, 3);
|
||||
REQUIRE_THAT(tr1, Catch::Matchers::WithinRel(tr0, 1e-6f));
|
||||
REQUIRE_THAT(frob(A), Catch::Matchers::WithinRel(frob(Aorig), 1e-6f));
|
||||
}
|
||||
|
||||
// Eigenvalues of the tridiagonal match the original (scipy reference):
|
||||
// 6.0, 4.0, 3.0, 1.0
|
||||
{
|
||||
Matrix<4, 1> vals{};
|
||||
Matrix<4, 4> vecs{};
|
||||
QR::EigenQR(A, vecs, vals, 10000, 1e-6f);
|
||||
REQUIRE_THAT(vals[0][0], Catch::Matchers::WithinRel(6.0f, 1e-4f));
|
||||
REQUIRE_THAT(vals[1][0], Catch::Matchers::WithinRel(4.0f, 1e-4f));
|
||||
REQUIRE_THAT(vals[2][0], Catch::Matchers::WithinRel(3.0f, 1e-4f));
|
||||
REQUIRE_THAT(vals[3][0], Catch::Matchers::WithinRel(1.0f, 1e-4f));
|
||||
}
|
||||
}
|
||||
|
||||
// 5x5 symmetric
|
||||
{
|
||||
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> Aorig = A;
|
||||
Matrix<5, 5> U{0};
|
||||
|
||||
QR::Tridiagonalize(A, U);
|
||||
|
||||
// All |i - j| >= 2 entries zero up to float32 roundoff
|
||||
for (uint8_t i = 0; i < 5; i++)
|
||||
for (uint8_t j = 0; j < 5; j++)
|
||||
if (i > j + 1 || j > i + 1)
|
||||
REQUIRE_THAT(A.Get(i, j), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
|
||||
REQUIRE(isOrthogonal(U));
|
||||
|
||||
Matrix<5, 5> UAt{};
|
||||
U.Mult(A, UAt);
|
||||
Matrix<5, 5> UAtU{};
|
||||
UAt.Mult(U.Transpose(), UAtU);
|
||||
for (uint8_t i = 0; i < 5; i++)
|
||||
for (uint8_t j = 0; j < 5; j++) {
|
||||
float actual = UAtU.Get(i, j);
|
||||
float expected = Aorig.Get(i, j);
|
||||
if (fabsf(expected) < 1e-3f)
|
||||
REQUIRE_THAT(actual, Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
else
|
||||
REQUIRE_THAT(actual,
|
||||
Catch::Matchers::WithinRel(expected, 1e-5f));
|
||||
}
|
||||
}
|
||||
|
||||
// Already tridiagonal: U must come out as the identity
|
||||
{
|
||||
Matrix<3, 3> A{1, 2, 0, 2, 5, 2, 0, 2, 9};
|
||||
Matrix<3, 3> U{0};
|
||||
QR::Tridiagonalize(A, U);
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
for (uint8_t j = 0; j < 3; j++) {
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
REQUIRE_THAT(U.Get(i, j), Catch::Matchers::WithinAbs(expected, 1e-7f));
|
||||
}
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// TEST 8: One full shifted QR step (integration of the blocks)
|
||||
// ============================================================================
|
||||
TEST_CASE("QR Building Block: Full Shifted QR Step", "[Matrix][QR]") {
|
||||
// One Wilkinson-shifted QR step on the whole 3x3 block is a similarity
|
||||
// transform, so all spectrum invariants (trace, sum of principal 2x2
|
||||
// minors, determinant) must be preserved.
|
||||
//
|
||||
// A = [[1,2,3],[2,5,8],[3,8,9]]: tr = 15, e2 = -18, det = -4
|
||||
{
|
||||
Matrix<3, 3> A{1, 2, 3, 2, 5, 8, 3, 8, 9};
|
||||
float tr0 = trace3(A); // 15
|
||||
float e20 = e2_3x3(A); // -18
|
||||
float det0 = det3(A); // -4
|
||||
|
||||
// mu from the trailing 2x2 [[5,8],[8,9]]: eigenvalues
|
||||
// -1.246211251, 15.246211251; closest to d = 9 is 15.246211251 (Wilkinson)
|
||||
float mu = QR::WilkinsonShift(A.Get(1, 1), A.Get(2, 1), A.Get(2, 2));
|
||||
REQUIRE_THAT(mu, Catch::Matchers::WithinRel(15.246211251f, 1e-5f));
|
||||
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
A[i][i] -= mu;
|
||||
|
||||
// Bulge chase: rotations on (0,1) then (1,2)
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(A.Get(0, 0), A.Get(1, 0), c, s);
|
||||
QR::ApplyRotationBothSides(A, 0, c, s);
|
||||
QR::GivensRotation(A.Get(1, 1), A.Get(2, 1), c, s);
|
||||
QR::ApplyRotationBothSides(A, 1, c, s);
|
||||
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
A[i][i] += mu;
|
||||
|
||||
// Symmetry preserved
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
for (uint8_t j = 0; j < 3; j++)
|
||||
REQUIRE(A.Get(i, j) == A.Get(j, i));
|
||||
|
||||
// Spectrum invariants preserved
|
||||
REQUIRE_THAT(trace3(A), Catch::Matchers::WithinRel(tr0, 1e-5f));
|
||||
REQUIRE_THAT(e2_3x3(A), Catch::Matchers::WithinRel(e20, 1e-5f));
|
||||
REQUIRE_THAT(det3(A), Catch::Matchers::WithinRel(det0, 1e-5f));
|
||||
}
|
||||
|
||||
// For TRIDIAGONAL input a single step keeps the tridiagonal structure
|
||||
{
|
||||
Matrix<3, 3> T{1, 2, 0, 2, 5, 2, 0, 2, 9};
|
||||
float mu = QR::WilkinsonShift(T.Get(1, 1), T.Get(2, 1), T.Get(2, 2));
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
T[i][i] -= mu;
|
||||
float c = 0, s = 0;
|
||||
QR::GivensRotation(T.Get(0, 0), T.Get(1, 0), c, s);
|
||||
QR::ApplyRotationBothSides(T, 0, c, s);
|
||||
QR::GivensRotation(T.Get(1, 1), T.Get(2, 1), c, s);
|
||||
QR::ApplyRotationBothSides(T, 1, c, s);
|
||||
for (uint8_t i = 0; i < 3; i++)
|
||||
T[i][i] += mu;
|
||||
|
||||
// Corners must vanish up to float32 roundoff: tridiagonal form
|
||||
// maintained. The cancellation is exact in exact arithmetic (the
|
||||
// corner is s1*a - c1*b times a factor, and Givens gives s1*a = c1*b),
|
||||
// so the residual is pure rounding, ~1e-6 for O(1) entries.
|
||||
REQUIRE_THAT(T.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
REQUIRE_THAT(T.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reference values for the QR eigen-decomposition building block tests
|
||||
(unit-tests/qr-build-blocks-tests.cpp). Run this to verify/implement the
|
||||
C++ implementation in src/QR.hpp / src/QR.cpp against numpy/scipy.
|
||||
|
||||
Conventions (match the C++ exactly):
|
||||
* Givens zeroing rotation: G = [[c, s], [-s, c]], c = x/r, s = y/r,
|
||||
r = hypot(x, y). G * (x, y)^T = (r, 0)^T.
|
||||
* Similarity transform: A <- G A G^T (ApplyRotationBothSides).
|
||||
* Eigenvector accumulation: V <- V G^T (ApplyRotationToVectors).
|
||||
Vblock in the 2x2 closed form is [[c, -s], [s, c]] (same shape as G^T).
|
||||
* Tridiagonalization: bottom-up Givens (i = N-2 down to k+1 per column k).
|
||||
* Shifted QR loop: Wilkinson shift mu from the trailing 2x2, chase on the
|
||||
trailing unreduced block [lo, hi], deflate by relative tolerance, peel
|
||||
exact-zero subdiagonals, 2x2 closed-form termination.
|
||||
* Pipeline: M0 = U * Mtri * U^T and Mtri = V * D * V^T =>
|
||||
eigenvectors of M0 = U * V (columns), eigenvalues = diag(D).
|
||||
|
||||
Usage: python3 qr-reference-values.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import scipy.linalg as sla
|
||||
|
||||
np.set_printoptions(precision=9, linewidth=120)
|
||||
|
||||
|
||||
def givens(x, y):
|
||||
"""c = x/r, s = y/r with r = hypot(x, y)."""
|
||||
r = np.hypot(x, y)
|
||||
if r == 0.0:
|
||||
return 1.0, 0.0
|
||||
return x / r, y / r
|
||||
|
||||
|
||||
def rot(n, i, c, s):
|
||||
"""G = I with [[c, s], [-s, c]] embedded at (i, i+1)."""
|
||||
G = np.eye(n)
|
||||
G[i:i + 2, i:i + 2] = np.array([[c, s], [-s, c]])
|
||||
return G
|
||||
|
||||
|
||||
def tridiagonalize(M0):
|
||||
"""Bottom-up Givens tridiagonalization. Returns (Mtri, U) with
|
||||
M0 = U Mtri U^T."""
|
||||
n = len(M0)
|
||||
M = M0.copy()
|
||||
U = np.eye(n)
|
||||
for k in range(n - 2):
|
||||
for i in range(n - 2, k, -1):
|
||||
c, s = givens(M[i, k], M[i + 1, k])
|
||||
G = rot(n, i, c, s)
|
||||
M = G @ M @ G.T
|
||||
U = U @ G.T
|
||||
return M, U
|
||||
|
||||
|
||||
def wilkinson(a, b, d):
|
||||
"""Eigenvalue of [[a, b], [b, d]] closest to d."""
|
||||
delta = 0.5 * (a - d)
|
||||
spread = np.sqrt(delta * delta + b * b)
|
||||
return 0.5 * (a + d) - (spread if delta >= 0 else -spread)
|
||||
|
||||
|
||||
def solve2x2(A, lo):
|
||||
"""Closed form for the block at (lo, lo+1): (lHi, lLo, c, s) with
|
||||
vHi = (c, s), vLo = (-s, c)."""
|
||||
a = A[lo, lo]
|
||||
b = A[lo, lo + 1]
|
||||
e = A[lo + 1, lo]
|
||||
d = A[lo + 1, lo + 1]
|
||||
tr = a + d
|
||||
det = a * d - b * e
|
||||
disc = max(0.0, tr * tr - 4 * det)
|
||||
lhi = 0.5 * (tr + np.sqrt(disc))
|
||||
llo = 0.5 * (tr - np.sqrt(disc))
|
||||
if b != 0.0:
|
||||
v1 = lhi - a
|
||||
nn = np.hypot(b, v1)
|
||||
c, s = b / nn, v1 / nn
|
||||
elif a >= d:
|
||||
c, s = 1.0, 0.0
|
||||
else:
|
||||
c, s = 0.0, 1.0
|
||||
return lhi, llo, c, s
|
||||
|
||||
|
||||
def eigenqr(M0, tol=1e-12, max_iter=100000):
|
||||
"""Full pipeline mirroring QR::EigenQR. Returns (eigs, W) where W has
|
||||
the eigenvectors of M0 as columns."""
|
||||
n = len(M0)
|
||||
if n == 2:
|
||||
l1, l2, c, s = solve2x2(M0, 0)
|
||||
return np.array([l1, l2]), np.array([[c, -s], [s, c]])
|
||||
M, U = tridiagonalize(M0)
|
||||
V = np.eye(n)
|
||||
hi = n - 1
|
||||
for _ in range(max_iter):
|
||||
# deflate: zero tiny subdiagonals (relative test)
|
||||
for i in range(hi):
|
||||
t = M[i + 1, i]
|
||||
scale = abs(M[i, i]) + abs(M[i + 1, i + 1])
|
||||
if abs(t) <= tol * scale:
|
||||
M[i + 1, i] = M[i, i + 1] = 0.0
|
||||
# peel exact-zero trailing subdiagonals
|
||||
while hi > 0 and M[hi, hi - 1] == 0.0:
|
||||
hi -= 1
|
||||
if hi == 0:
|
||||
break
|
||||
# find start of trailing unreduced block
|
||||
lo = hi
|
||||
for i in range(hi - 1, -1, -1):
|
||||
if M[i + 1, i] == 0.0:
|
||||
break
|
||||
lo = i
|
||||
if lo + 1 == hi:
|
||||
# closed-form 2x2 termination: set diagonal, fold Vblock in
|
||||
l1, l2, c, s = solve2x2(M, lo)
|
||||
Vb = np.eye(n)
|
||||
Vb[lo:lo + 2, lo:lo + 2] = np.array([[c, -s], [s, c]])
|
||||
V = V @ Vb
|
||||
M[lo, lo] = l1
|
||||
M[lo + 1, lo + 1] = l2
|
||||
M[lo + 1, lo] = M[lo, lo + 1] = 0.0
|
||||
if lo == 0:
|
||||
break
|
||||
hi = lo - 1
|
||||
continue
|
||||
# full shifted step on [lo, hi] (shift applies to the active block)
|
||||
mu = wilkinson(M[hi - 1, hi - 1], M[hi, hi - 1], M[hi, hi])
|
||||
diag = M.diagonal().copy()
|
||||
diag[lo:hi + 1] -= mu
|
||||
np.fill_diagonal(M, diag)
|
||||
c, s = givens(M[lo, lo], M[lo + 1, lo])
|
||||
G = rot(n, lo, c, s)
|
||||
M = G @ M @ G.T
|
||||
V = V @ G.T
|
||||
for i in range(lo + 1, hi):
|
||||
c, s = givens(M[i, i], M[i + 1, i])
|
||||
G = rot(n, i, c, s)
|
||||
M = G @ M @ G.T
|
||||
V = V @ G.T
|
||||
diag = M.diagonal().copy()
|
||||
diag[lo:hi + 1] += mu
|
||||
np.fill_diagonal(M, diag)
|
||||
|
||||
eigs = np.diag(M).astype(float)
|
||||
order = np.argsort(eigs)[::-1] # descending, like the C++ test harness
|
||||
eigs = eigs[order]
|
||||
W = U @ V
|
||||
W = W[:, order]
|
||||
return eigs, W
|
||||
|
||||
|
||||
def report(name, val, ref=None, tol=1e-6):
|
||||
ok = "OK " if ref is None or np.allclose(val, ref, rtol=tol, atol=tol) else "FAIL"
|
||||
print(f"[{ok}] {name} = {val}")
|
||||
if ref is not None:
|
||||
print(f" scipy/numpy ref = {ref}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=== TEST 1: GivensRotation ===")
|
||||
c, s = givens(2.0, 1.0)
|
||||
print(f" c = {c} s = {s}")
|
||||
# G * (x, y)^T = (r, 0)^T: G = [[c, s], [-s, c]]
|
||||
assert abs(c * 2 + s * 1 - np.sqrt(5)) < 1e-15
|
||||
assert abs(-s * 2 + c * 1) < 1e-15
|
||||
|
||||
print("\n=== TEST 2: ApplyRotationBothSides A <- G A G^T ===")
|
||||
A = np.array([[3.0, 4.0, 5.0], [6.0, 7.0, 8.0], [9.0, 10.0, 11.0]])
|
||||
G = rot(3, 0, 0.6, 0.8)
|
||||
B = G @ A @ G.T
|
||||
print(B)
|
||||
|
||||
A = np.array([[5.0, 0.0, 1.0], [0.0, 6.0, 2.0], [1.0, 2.0, 7.0]])
|
||||
c, s = givens(6.0, 2.0)
|
||||
G = rot(3, 1, c, s)
|
||||
B = G @ A @ G.T
|
||||
print(B)
|
||||
|
||||
print("\n=== TEST 3: V accumulation V <- V G^T ===")
|
||||
V = np.eye(3)
|
||||
G = rot(3, 0, 0.894427191, 0.447213595)
|
||||
V = V @ G.T
|
||||
print(V)
|
||||
V2 = V @ rot(3, 1, 0.848874681, 0.528748047).T
|
||||
print(V2)
|
||||
|
||||
print("\n=== TEST 4: Solve2x2Eigen ===")
|
||||
for A in (np.array([[5.0, 8.0], [8.0, 9.0]]), np.array([[1.0, 2.0], [3.0, 4.0]])):
|
||||
l1, l2, c, s = solve2x2(A, 0)
|
||||
ref = np.linalg.eigvalsh(A) if np.allclose(A, A.T) else np.linalg.eigvals(A)
|
||||
print(f" A={A.ravel()} lHi={l1} lLo={l2} c={c} s={s} ref={np.sort(ref)[::-1]}")
|
||||
|
||||
print("\n=== TEST 8: WilkinsonShift ===")
|
||||
print(f" W(5, 8, 9) = {wilkinson(5, 8, 9)}")
|
||||
print(f" W(4, 2, 7) = {wilkinson(4, 2, 7)}")
|
||||
print(f" W(9, 2, 5) = {wilkinson(9, 2, 5)}")
|
||||
|
||||
print("\n=== TEST 8b: one full shifted chase step on tridiagonal 3x3 ===")
|
||||
T = np.array([[1.0, 2.0, 0.0], [2.0, 5.0, 2.0], [0.0, 2.0, 9.0]])
|
||||
mu = wilkinson(5, 2, 9)
|
||||
M = T - mu * np.eye(3)
|
||||
c, s = givens(M[0, 0], M[1, 0])
|
||||
M = rot(3, 0, c, s) @ M @ rot(3, 0, c, s).T
|
||||
c, s = givens(M[1, 1], M[2, 1])
|
||||
M = rot(3, 1, c, s) @ M @ rot(3, 1, c, s).T
|
||||
M = M + mu * np.eye(3)
|
||||
print(f" mu = {mu}")
|
||||
print(M)
|
||||
print(f" corners: {M[0, 2]}, {M[2, 0]} (exact-arithmetic zeros)")
|
||||
print(f" trace {M.trace():.15f} (was {T.trace()})")
|
||||
|
||||
print("\n=== TEST 7: Tridiagonalize ===")
|
||||
M4 = np.array([[2.0, 1, 0, 1], [1, 3, 1, 0], [0, 1, 4, 1], [1, 0, 1, 5]])
|
||||
M, U = tridiagonalize(M4)
|
||||
print(" M4 tridiagonalized:\n", M)
|
||||
print(f" reconstruction U M U^T == M4: {np.allclose(U @ M @ U.T, M4, atol=1e-9)}")
|
||||
M5 = np.array([[3.0, 1, 0, 0, 1], [1, 4, 1, 0, 0], [0, 1, 5, 1, 0],
|
||||
[0, 0, 1, 6, 1], [1, 0, 0, 1, 7]])
|
||||
M, U = tridiagonalize(M5)
|
||||
print(" M5 tridiagonalized:\n", M)
|
||||
print(f" reconstruction: {np.allclose(U @ M @ U.T, M5, atol=1e-9)}")
|
||||
|
||||
print("\n=== End-to-end: random symmetric vs scipy.linalg.eigh ===")
|
||||
rng = np.random.default_rng(12345)
|
||||
worst = 0.0
|
||||
for n in range(3, 9):
|
||||
M0 = rng.normal(size=(n, n))
|
||||
M0 = (M0 + M0.T) / 2
|
||||
eigs, W = eigenqr(M0.astype(float))
|
||||
ref = sla.eigh(M0)
|
||||
e_err = np.max(np.abs(np.sort(eigs) - ref[0]))
|
||||
resid = np.linalg.norm(W @ np.diag(eigs) @ W.T - M0)
|
||||
ortho = np.linalg.norm(W.T @ W - np.eye(n))
|
||||
print(f" n={n}: eigs_err={e_err:.2e} resid={resid:.2e} ortho={ortho:.2e}")
|
||||
worst = max(worst, e_err, resid, ortho)
|
||||
print(f"\nworst over all n: {worst:.2e}")
|
||||
assert worst < 1e-10, "end-to-end reference FAILED"
|
||||
print("ALL REFERENCES OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,363 @@
|
||||
#include "Matrix.hpp"
|
||||
#include "SVD.hpp"
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <iostream>
|
||||
|
||||
// Generic helper functions for any matrix size
|
||||
template <uint8_t rows, uint8_t columns>
|
||||
static float frobeniusNorm(const Matrix<rows, columns> &M) {
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < rows; i++)
|
||||
for (int j = 0; j < columns; j++) {
|
||||
float v = M.Get(i, j);
|
||||
sum += v * v;
|
||||
}
|
||||
return sqrtf(sum);
|
||||
}
|
||||
|
||||
template <uint8_t n>
|
||||
static bool isOrthogonal(const Matrix<n, n> &M, float tol = 1e-4f) {
|
||||
Matrix<n, n> Mt = M.Transpose();
|
||||
Matrix<n, n> MtM{0};
|
||||
Mt.Mult(M, MtM);
|
||||
for (int i = 0; i < n; i++)
|
||||
for (int j = 0; j < n; j++) {
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
if (fabsf(MtM.Get(i, j) - expected) > tol)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: 2x2 [[1,2],[3,4]]", "[Matrix][SVD][Integration]") {
|
||||
Matrix<2, 2> A{1, 2, 3, 4};
|
||||
Matrix<2, 2> U{0};
|
||||
Matrix<2, 1> sigma{0};
|
||||
Matrix<2, 2> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// Reference singular values from scipy: [5.464985704219, 0.365966190626]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.4649857f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(0.3659662f, 1e-3f));
|
||||
|
||||
// Check orthogonality of U and Vt (first 2x2 blocks)
|
||||
REQUIRE(isOrthogonal<2>(U));
|
||||
REQUIRE(isOrthogonal<2>(Vt));
|
||||
|
||||
// Check reconstruction: A ≈ U · diag(sigma) · Vt
|
||||
Matrix<2, 2> recon{0};
|
||||
Matrix<2, 2> Usig{0};
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int j = 0; j < 2; j++)
|
||||
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||
|
||||
Usig.Mult(Vt, recon);
|
||||
|
||||
float err = 0.0f;
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int j = 0; j < 2; j++) {
|
||||
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||
err += diff * diff;
|
||||
}
|
||||
err = sqrtf(err);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
std::cout << "SVD 2x2 [[1,2],[3,4]]:\n";
|
||||
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
|
||||
<< "]\n";
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: 3x3 diagonal [10,5,2]",
|
||||
"[Matrix][SVD][Integration]") {
|
||||
Matrix<3, 3> A{10, 0, 0, 0, 5, 0, 0, 0, 2};
|
||||
Matrix<3, 3> U{0};
|
||||
Matrix<3, 1> sigma{0};
|
||||
Matrix<3, 3> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// Singular values should be [10, 5, 2] (already diagonal)
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
|
||||
|
||||
// U and Vt should be identity (or close) for diagonal matrix
|
||||
float uErr = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||
float vtErr = frobeniusNorm(Vt - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||
REQUIRE_THAT(uErr, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
REQUIRE_THAT(vtErr, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: 3x3 rank-deficient [[1,2,3],[4,5,6],[7,8,9]]",
|
||||
"[Matrix][SVD][Integration]") {
|
||||
Matrix<3, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
Matrix<3, 3> U{0};
|
||||
Matrix<3, 1> sigma{0};
|
||||
Matrix<3, 3> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// Reference: [16.848103352614, 1.068369514555, 0.0]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(16.8481f, 1e-2f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.06837f, 1e-2f));
|
||||
// Third singular value should be ~0 (rank-deficient)
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
// Check reconstruction
|
||||
Matrix<3, 3> recon{0};
|
||||
Matrix<3, 3> Usig{0};
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||
Usig.Mult(Vt, recon);
|
||||
|
||||
float err = 0.0f;
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++) {
|
||||
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||
err += diff * diff;
|
||||
}
|
||||
err = sqrtf(err);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
std::cout << "SVD 3x3 rank-deficient:\n";
|
||||
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||
<< sigma.Get(2, 0) << "]\n";
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: tall 4x3 matrix", "[Matrix][SVD][Integration]") {
|
||||
Matrix<4, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
|
||||
Matrix<4, 3> U{0};
|
||||
Matrix<3, 1> sigma{0};
|
||||
Matrix<3, 3> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// Reference: [25.462407436036, 1.290661675761, 0.0]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(25.4624f, 1e-2f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.29066f, 1e-2f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
// Check reconstruction
|
||||
Matrix<4, 3> recon{0};
|
||||
Matrix<4, 3> Usig{0};
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||
Usig.Mult(Vt, recon);
|
||||
|
||||
float err = 0.0f;
|
||||
for (int i = 0; i < 4; i++)
|
||||
for (int j = 0; j < 3; j++) {
|
||||
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||
err += diff * diff;
|
||||
}
|
||||
err = sqrtf(err);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
std::cout << "SVD tall 4x3:\n";
|
||||
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||
<< sigma.Get(2, 0) << "]\n";
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: wide 3x5 matrix", "[Matrix][SVD][Integration]") {
|
||||
Matrix<3, 5> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
|
||||
Matrix<3, 5> U{0};
|
||||
Matrix<5, 1> sigma{0}; // sigma is columns x 1 = 5x1 for wide matrix
|
||||
Matrix<5, 5> Vt{0}; // Vt is columns x columns = 5x5
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// Reference: [35.127223333575, 2.465396696917, 0.0]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(35.1272f, 1e-2f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.46540f, 1e-2f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
// Check reconstruction: A (3x5) = U * Sigma * Vt, where U (3x5) has
|
||||
// its meaningful part in the first 3 columns, sigma (5x1) in the
|
||||
// first 3 entries, and Vt (5x5) in its first 3 rows (right
|
||||
// singular vectors as rows). So:
|
||||
// A[i][j] = sum_k U[i][k] * sigma[k] * Vt[k][j]
|
||||
|
||||
float err2 = 0.0f;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 5; j++) {
|
||||
float recon_val = 0.0f;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
recon_val += U.Get(i, k) * sigma.Get(k, 0) * Vt.Get(k, j);
|
||||
}
|
||||
float diff = recon_val - A.Get(i, j);
|
||||
err2 += diff * diff;
|
||||
}
|
||||
}
|
||||
err2 = sqrtf(err2);
|
||||
REQUIRE_THAT(err2, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
|
||||
std::cout << "SVD wide 3x5:\n";
|
||||
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||
<< sigma.Get(2, 0) << "]\n";
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: identity 3x3", "[Matrix][SVD][Integration]") {
|
||||
Matrix<3, 3> A{1, 0, 0, 0, 1, 0, 0, 0, 1};
|
||||
Matrix<3, 3> U{0};
|
||||
Matrix<3, 1> sigma{0};
|
||||
Matrix<3, 3> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||
|
||||
float err = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
}
|
||||
|
||||
TEST_CASE("SVD Integration: symmetric positive definite 2x2 [[5,3],[3,5]]",
|
||||
"[Matrix][SVD][Integration]") {
|
||||
Matrix<2, 2> A{5, 3, 3, 5};
|
||||
Matrix<2, 2> U{0};
|
||||
Matrix<2, 1> sigma{0};
|
||||
Matrix<2, 2> Vt{0};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
// For SPD matrix, singular values = eigenvalues: [8, 2]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(8.0f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
|
||||
|
||||
// Check reconstruction
|
||||
Matrix<2, 2> recon{0};
|
||||
Matrix<2, 2> Usig{0};
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int j = 0; j < 2; j++)
|
||||
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||
Usig.Mult(Vt, recon);
|
||||
|
||||
float err = 0.0f;
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int j = 0; j < 2; j++) {
|
||||
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||
err += diff * diff;
|
||||
}
|
||||
err = sqrtf(err);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
std::cout << "SVD SPD 2x2 [[5,3],[3,5]]:\n";
|
||||
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
|
||||
<< "]\n";
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Matrix::SVD member wrapper (delegates to SVD::SVD)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reconstruction error ‖U·diag(sigma)·Vᵀ − A‖_F. Zero-padded entries of
|
||||
* U/sigma/Vt (wide/tall cases) are zero by the output conventions, so the
|
||||
* full product equals U[:, :k]·diag(sigma[:k])·Vt[:k, :].
|
||||
*/
|
||||
template <uint8_t rows, uint8_t columns>
|
||||
static float svdReconstructionError(const Matrix<rows, columns> &A,
|
||||
const Matrix<rows, columns> &U,
|
||||
const Matrix<columns, 1> &sigma,
|
||||
const Matrix<columns, columns> &Vt) {
|
||||
Matrix<rows, columns> recon{0};
|
||||
Matrix<rows, columns> Usig{0};
|
||||
for (int i = 0; i < rows; i++)
|
||||
for (int j = 0; j < columns; j++)
|
||||
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||
Usig.Mult(Vt, recon);
|
||||
float err = 0.0f;
|
||||
for (int i = 0; i < rows; i++)
|
||||
for (int j = 0; j < columns; j++) {
|
||||
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||
err += diff * diff;
|
||||
}
|
||||
return sqrtf(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Orthonormality of the first k columns of M: the k×k leading block of
|
||||
* MᵀM must equal I_k. (For a tall SVD, U has k = min(rows, cols)
|
||||
* meaningful columns and this is the full UᵀU.)
|
||||
*/
|
||||
template <uint8_t r, uint8_t c>
|
||||
static bool leadingColumnsOrthonormal(const Matrix<r, c> &M, uint8_t k,
|
||||
float tol = 1e-4f) {
|
||||
Matrix<c, r> Mt = M.Transpose();
|
||||
Matrix<c, c> MtM{0};
|
||||
Mt.Mult(M, MtM);
|
||||
for (int i = 0; i < k; i++)
|
||||
for (int j = 0; j < k; j++) {
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
if (fabsf(MtM.Get(i, j) - expected) > tol)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orthonormality of the first k rows of M: the k×k leading block of
|
||||
* M·Mᵀ must equal I_k. (Vᵀ may have zero-padded trailing rows in the
|
||||
* wide case, so check only the meaningful leading block.)
|
||||
*/
|
||||
template <uint8_t r, uint8_t c>
|
||||
static bool leadingRowsOrthonormal(const Matrix<r, c> &M, uint8_t k,
|
||||
float tol = 1e-4f) {
|
||||
Matrix<c, r> Mt = M.Transpose();
|
||||
Matrix<r, r> MMt{0};
|
||||
M.Mult(Mt, MMt);
|
||||
for (int i = 0; i < k; i++)
|
||||
for (int j = 0; j < k; j++) {
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
if (fabsf(MMt.Get(i, j) - expected) > tol)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST_CASE("Matrix::SVD wrapper: 3x2 tall [[1,2],[3,4],[5,6]]",
|
||||
"[Matrix][SVD][Wrapper]") {
|
||||
Matrix<3, 2> A{1, 2, 3, 4, 5, 6};
|
||||
Matrix<3, 2> U{0};
|
||||
Matrix<2, 1> sigma{0};
|
||||
Matrix<2, 2> Vt{0};
|
||||
|
||||
A.SVD(U, sigma, Vt);
|
||||
|
||||
// Reference singular values from numpy: [9.52552, 0.514301]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(9.52552f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(0.514301f, 1e-3f));
|
||||
|
||||
REQUIRE(leadingColumnsOrthonormal(U, 2));
|
||||
REQUIRE(leadingRowsOrthonormal(Vt, 2));
|
||||
|
||||
float err = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("Matrix::SVD wrapper: 2x3 wide [[1,2,3],[4,5,6]]",
|
||||
"[Matrix][SVD][Wrapper]") {
|
||||
Matrix<2, 3> A{1, 2, 3, 4, 5, 6};
|
||||
Matrix<2, 3> U{0};
|
||||
Matrix<3, 1> sigma{0};
|
||||
Matrix<3, 3> Vt{0};
|
||||
|
||||
A.SVD(U, sigma, Vt);
|
||||
|
||||
// Reference singular values from numpy: [9.50803, 0.77287]; the third
|
||||
// entry (wide-matrix padding) must be zero.
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(9.50803f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(0.77287f, 1e-3f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
|
||||
REQUIRE(leadingColumnsOrthonormal(U, 2));
|
||||
REQUIRE(leadingRowsOrthonormal(Vt, 2));
|
||||
|
||||
float err = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate reference values for SVD building block unit tests.
|
||||
Run this to verify/implement the C++ SVD implementation against scipy/numpy.
|
||||
|
||||
Usage: python3 svd-reference-values.py
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.linalg import svd, qr as scipy_qr
|
||||
import json
|
||||
|
||||
def compute_householder(x):
|
||||
"""Compute Householder reflector: H*x = [alpha, 0, 0, ...]^T.
|
||||
|
||||
Returns (v_normalized, alpha) where v is the normalized Householder vector.
|
||||
H = I - 2*v*v^T / (v^T*v)
|
||||
"""
|
||||
x = np.array(x, dtype=np.float64)
|
||||
norm_x = np.linalg.norm(x)
|
||||
|
||||
if norm_x < 1e-30:
|
||||
return x.copy(), 0.0
|
||||
|
||||
alpha = -np.sign(x[0]) * norm_x if x[0] != 0 else -norm_x
|
||||
v = x.copy()
|
||||
v[0] -= alpha
|
||||
v_norm = np.linalg.norm(v)
|
||||
|
||||
if v_norm < 1e-30:
|
||||
return np.zeros_like(x), alpha
|
||||
|
||||
v /= v_norm
|
||||
return v, alpha
|
||||
|
||||
def apply_householder_left(A, v, start_row):
|
||||
"""Apply Householder reflection from the left: A = (I - 2vv^T) @ A.
|
||||
|
||||
v is the normalized Householder vector operating on rows [start_row:].
|
||||
The length of v must match the number of rows affected.
|
||||
"""
|
||||
A = A.copy()
|
||||
k = len(v)
|
||||
|
||||
for col in range(A.shape[1]):
|
||||
dot = np.dot(v, A[start_row:start_row+k, col])
|
||||
A[start_row:start_row+k, col] -= 2.0 * dot * v
|
||||
|
||||
return A
|
||||
|
||||
def apply_householder_right(A, v, start_col):
|
||||
"""Apply Householder reflection from the right: A = A @ (I - 2vv^T).
|
||||
|
||||
v is the normalized Householder vector operating on columns [start_col:].
|
||||
The length of v must match the number of columns affected.
|
||||
"""
|
||||
A = A.copy()
|
||||
k = len(v)
|
||||
|
||||
for row in range(A.shape[0]):
|
||||
dot = np.dot(A[row, start_col:start_col+k], v)
|
||||
A[row, start_col:start_col+k] -= 2.0 * dot * v
|
||||
|
||||
return A
|
||||
|
||||
def compute_givens(x, y):
|
||||
"""Compute Givens rotation that zeros out y.
|
||||
|
||||
Returns (c, s) such that [c s; -s c] @ [x; y] = [r; 0].
|
||||
"""
|
||||
r = np.sqrt(x*x + y*y)
|
||||
if r < 1e-30:
|
||||
return 1.0, 0.0
|
||||
c = x / r
|
||||
s = y / r
|
||||
return c, s
|
||||
|
||||
def apply_givens_left(A, i, j, c, s):
|
||||
"""Apply Givens rotation from the left to rows i and j of A.
|
||||
|
||||
[c s] [row_i]
|
||||
[-s c] @ [row_j] = [new_row_i]
|
||||
[new_row_j]
|
||||
"""
|
||||
A = A.copy()
|
||||
new_i = c * A[i] + s * A[j]
|
||||
new_j = -s * A[i] + c * A[j]
|
||||
A[i] = new_i
|
||||
A[j] = new_j
|
||||
return A
|
||||
|
||||
def apply_givens_right(A, i, j, c, s):
|
||||
"""Apply Givens rotation from the right to columns i and j of A.
|
||||
|
||||
[col_i col_j] @ [c -s] = [new_col_i new_col_j]
|
||||
[s c]
|
||||
"""
|
||||
A = A.copy()
|
||||
new_i = c * A[:, i] + s * A[:, j]
|
||||
new_j = -s * A[:, i] + c * A[:, j]
|
||||
A[:, i] = new_i
|
||||
A[:, j] = new_j
|
||||
return A
|
||||
|
||||
def householder_bidiagonalization(A):
|
||||
"""Full Householder bidiagonalization: A = Q_L @ B @ Q_R^T.
|
||||
|
||||
Returns (B, Q_L, Q_R) where B is upper bidiagonal.
|
||||
"""
|
||||
m, n = A.shape
|
||||
p = min(m, n)
|
||||
|
||||
QL = np.eye(m, dtype=np.float64)
|
||||
QR = np.eye(n, dtype=np.float64)
|
||||
W = A.copy()
|
||||
|
||||
for k in range(p):
|
||||
# Left HH: zero out W[k+1:, k]
|
||||
if k < m - 1:
|
||||
x = W[k+1:, k].copy()
|
||||
v, alpha = compute_householder(x)
|
||||
if np.linalg.norm(v) > 1e-30:
|
||||
W = apply_householder_left(W, v, k + 1)
|
||||
QL = apply_householder_right(QL, v, k + 1)
|
||||
|
||||
# Right HH: zero out W[k, k+2:] (superdiagonal)
|
||||
if k < p - 1 and k + 2 <= n:
|
||||
x = W[k, k+2:].copy()
|
||||
v, alpha = compute_householder(x)
|
||||
if np.linalg.norm(v) > 1e-30:
|
||||
W = apply_householder_right(W, v, k + 2)
|
||||
QR = apply_householder_right(QR, v, k + 2)
|
||||
|
||||
return W, QL, QR
|
||||
|
||||
def implicit_qr_iteration(B, QR_acc):
|
||||
"""Implicit QR iteration on a bidiagonal matrix.
|
||||
|
||||
Returns (Sigma, QR_acc) where Sigma is diagonal with singular values
|
||||
and QR_acc contains the accumulated right transformations.
|
||||
"""
|
||||
m, n = B.shape
|
||||
p = min(m, n)
|
||||
W = B.copy()
|
||||
|
||||
max_iter = 1000
|
||||
tol = 1e-10
|
||||
|
||||
for iteration in range(max_iter):
|
||||
# Deflate negligible subdiagonal elements
|
||||
for i in range(p - 1, 0, -1):
|
||||
if abs(W[i, i-1]) < tol * (abs(W[i-1, i-1]) + abs(W[i, i])):
|
||||
W[i, i-1] = 0.0
|
||||
|
||||
# Find smallest unreduced block [start, end]
|
||||
start = 0
|
||||
for i in range(p - 1):
|
||||
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
|
||||
start = i + 1
|
||||
|
||||
end = p - 1
|
||||
for i in range(p - 2, -1, -1):
|
||||
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
|
||||
end = i
|
||||
break
|
||||
|
||||
if start >= end:
|
||||
continue
|
||||
|
||||
# Wilkinson shift from bottom 2x2 corner
|
||||
a, b = W[end-1, end-1], W[end-1, end]
|
||||
c_val, d = W[end, end-1], W[end, end]
|
||||
trace = a + d
|
||||
det = a * d - b * c_val
|
||||
disc = trace**2 - 4 * det
|
||||
|
||||
if disc >= 0:
|
||||
sqrt_disc = np.sqrt(disc)
|
||||
e1, e2 = (trace + sqrt_disc) / 2, (trace - sqrt_disc) / 2
|
||||
shift = e1 if abs(e1 - d) < abs(e2 - d) else e2
|
||||
else:
|
||||
shift = d
|
||||
|
||||
# Implicit QR step using Givens rotations
|
||||
# Process from top to bottom within the block
|
||||
x = W[start, start] - shift
|
||||
y = W[start + 1, start]
|
||||
|
||||
for i in range(start, end):
|
||||
r = np.sqrt(x*x + y*y)
|
||||
if r < 1e-30:
|
||||
x = W[i + 1, i]
|
||||
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
|
||||
continue
|
||||
|
||||
c_rot = x / r
|
||||
s_rot = y / r
|
||||
|
||||
# Apply from left to rows i, i+1 (columns i..n-1)
|
||||
for j in range(i, n):
|
||||
t1, t2 = W[i, j], W[i + 1, j]
|
||||
W[i, j] = c_rot * t1 + s_rot * t2
|
||||
W[i + 1, j] = -s_rot * t1 + c_rot * t2
|
||||
|
||||
# Apply from right to columns i, i+1 (rows 0..i)
|
||||
if i > start:
|
||||
for j in range(i + 1):
|
||||
t1, t2 = W[j, i], W[j, i + 1]
|
||||
W[j, i] = c_rot * t1 + s_rot * t2
|
||||
W[j, i + 1] = -s_rot * t1 + c_rot * t2
|
||||
|
||||
# Accumulate into QR_acc
|
||||
for j in range(QR_acc.shape[0]):
|
||||
t1, t2 = QR_acc[j, i], QR_acc[j, i + 1]
|
||||
QR_acc[j, i] = c_rot * t1 + s_rot * t2
|
||||
QR_acc[j, i + 1] = -s_rot * t1 + c_rot * t2
|
||||
|
||||
# Prepare for next rotation
|
||||
x = W[i + 1, i]
|
||||
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
|
||||
|
||||
return W, QR_acc
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("SVB BUILDING BLOCK REFERENCE VALUES")
|
||||
print("Generated with scipy/numpy for C++ unit test verification")
|
||||
print("=" * 70)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 1: Householder Vector Computation
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 1: computeHouseholderVector")
|
||||
print("=" * 70)
|
||||
|
||||
test_vectors = [
|
||||
("2D [1,3]", [1.0, 3.0]),
|
||||
("2D [3,4] (norm=5)", [3.0, 4.0]),
|
||||
("3D [1,2,3]", [1.0, 2.0, 3.0]),
|
||||
("3D [0,0,1]", [0.0, 0.0, 1.0]),
|
||||
("4D [5,-3,2,1]", [5.0, -3.0, 2.0, 1.0]),
|
||||
]
|
||||
|
||||
for name, vec in test_vectors:
|
||||
v, alpha = compute_householder(vec)
|
||||
x = np.array(vec)
|
||||
Hx = x - 2 * np.dot(v, x) * v
|
||||
|
||||
print(f"\n{name}:")
|
||||
print(f" Input: {list(x)}")
|
||||
print(f" ||x||: {np.linalg.norm(x):.15f}")
|
||||
print(f" alpha: {alpha:.15f}")
|
||||
print(f" v (normalized): {[round(float(vi), 12) for vi in v]}")
|
||||
print(f" H*x = [alpha,0..]: {[round(float(xi), 12) for xi in Hx]}")
|
||||
print(f" Off-diagonal ~0: {np.allclose(Hx[1:], 0, atol=1e-12)}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 2: Householder Apply Left
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 2: applyHouseholderLeft")
|
||||
print("=" * 70)
|
||||
|
||||
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
|
||||
x_col = A_test[1:, 0].copy()
|
||||
v_left, _ = compute_householder(x_col)
|
||||
|
||||
print(f"\nInput matrix:\n{A_test}")
|
||||
print(f"Householder vector (rows 1:3): {[round(float(vi), 12) for vi in v_left]}")
|
||||
|
||||
A_result = apply_householder_left(A_test, v_left, 1)
|
||||
print(f"\nAfter applyHouseholderLeft:\n{A_result}")
|
||||
print(f" A[1,0] = {A_result[1,0]:.2e}, A[2,0] = {A_result[2,0]:.2e} (should be ~0)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 3: Householder Apply Right
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 3: applyHouseholderRight")
|
||||
print("=" * 70)
|
||||
|
||||
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
|
||||
x_row = A_test[0, 1:].copy()
|
||||
v_right, _ = compute_householder(x_row)
|
||||
|
||||
print(f"\nInput matrix:\n{A_test}")
|
||||
print(f"Householder vector (cols 1:3): {[round(float(vi), 12) for vi in v_right]}")
|
||||
|
||||
A_result = apply_householder_right(A_test, v_right, 1)
|
||||
print(f"\nAfter applyHouseholderRight:\n{A_result}")
|
||||
print(f" A[0,1] = {A_result[0,1]:.2e}, A[0,2] = {A_result[0,2]:.2e} (should be ~0)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 4: Givens Rotation Computation
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 4: computeGivens")
|
||||
print("=" * 70)
|
||||
|
||||
givens_tests = [
|
||||
("3-4-5 triangle", 3.0, 4.0),
|
||||
("y already zero", 1.0, 0.0),
|
||||
("x is zero", 0.0, 5.0),
|
||||
("Both negative", -3.0, -4.0),
|
||||
("45 degree case", 1.0, -1.0),
|
||||
]
|
||||
|
||||
for name, x, y in givens_tests:
|
||||
c, s = compute_givens(x, y)
|
||||
result_x = c * x + s * y
|
||||
result_y = -s * x + c * y
|
||||
|
||||
print(f"\n{name}: x={x}, y={y}")
|
||||
print(f" r = {np.sqrt(x*x+y*y):.12f}")
|
||||
print(f" c = {c:.12f}, s = {s:.12f}")
|
||||
print(f" [c s; -s c] @ [x;y] = [{result_x:.2e}, {result_y:.2e}]")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 5: Apply Givens Left/Right
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 5: applyGivensLeft / applyGivensRight")
|
||||
print("=" * 70)
|
||||
|
||||
A_test = np.array([[3.0, 4.0], [1.0, 2.0]], dtype=np.float64)
|
||||
c, s = compute_givens(3.0, 1.0)
|
||||
|
||||
print(f"\nInput matrix:\n{A_test}")
|
||||
print(f"Givens rotation (rows 0,1): c={c:.12f}, s={s:.12f}")
|
||||
|
||||
A_left = apply_givens_left(A_test, 0, 1, c, s)
|
||||
print(f"\nAfter applyGivensLeft:\n{A_left}")
|
||||
print(f" A[1,0] = {A_left[1,0]:.2e} (should be ~0)")
|
||||
|
||||
A_test = np.array([[3.0, 1.0], [4.0, 2.0]], dtype=np.float64)
|
||||
c, s = compute_givens(3.0, 4.0)
|
||||
|
||||
print(f"\nInput matrix:\n{A_test}")
|
||||
print(f"Givens rotation (cols 0,1): c={c:.12f}, s={s:.12f}")
|
||||
|
||||
A_right = apply_givens_right(A_test, 0, 1, c, s)
|
||||
print(f"\nAfter applyGivensRight:\n{A_right}")
|
||||
print(f" A[0,1] = {A_right[0,1]:.2e} (should be ~0)")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 6: Full Bidiagonalization
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 6: householderBidiagonalization")
|
||||
print("=" * 70)
|
||||
|
||||
bidiag_tests = [
|
||||
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
|
||||
("3x3 SPD [[5,3],[3,5]]", np.array([[5.0, 3.0], [3.0, 5.0]])),
|
||||
("3x3 diag [[10,0,0],[0,5,0],[0,0,2]]",
|
||||
np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
|
||||
("3x3 full [[1,2,3],[4,5,6],[7,8,10]]",
|
||||
np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])),
|
||||
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
|
||||
]
|
||||
|
||||
for name, A in bidiag_tests:
|
||||
B, QL, QR = householder_bidiagonalization(A)
|
||||
m, n = A.shape
|
||||
p = min(m, n)
|
||||
|
||||
print(f"\n{name}:")
|
||||
print(f" Original:\n{A}")
|
||||
print(f"\n Bidiagonal B:\n{B}")
|
||||
print(f" Diagonal: {[round(float(B[i,i]), 10) for i in range(p)]}")
|
||||
print(f" Superdiag: {[round(float(B[i,i+1]), 10) for i in range(min(p-1, n-1))]}")
|
||||
|
||||
recon = QL @ B @ QR.T
|
||||
err = np.linalg.norm(recon - A, 'fro')
|
||||
print(f" ||QL @ B @ QR^T - A||_F = {err:.2e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 7: Full SVD Reference Values
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 7: Full SVD Reference Values (scipy.linalg.svd)")
|
||||
print("=" * 70)
|
||||
|
||||
test_matrices = [
|
||||
("Simple 2x2", np.array([[1,2],[3,4]], dtype=np.float64)),
|
||||
("SPD 2x2", np.array([[5,3],[3,5]], dtype=np.float64)),
|
||||
("Full-rank 3x3", np.array([[1,2,3],[4,5,6],[7,8,10]], dtype=np.float64)),
|
||||
("Rank-deficient 3x3", np.array([[1,2,3],[4,5,6],[7,8,9]], dtype=np.float64)),
|
||||
("Diagonal 3x3", np.array([[10,0,0],[0,5,0],[0,0,2]], dtype=np.float64)),
|
||||
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
|
||||
("Wide 3x5", np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]], dtype=np.float64)),
|
||||
("Symmetric tri 5x5", np.array([[2,-1,0,0,0],[-1,2,-1,0,0],[0,-1,2,-1,0],[0,0,-1,2,-1],[0,0,0,-1,2]], dtype=np.float64)),
|
||||
("Neg values 2x3", np.array([[0.5,-0.3,0.8],[-0.2,0.7,0.1]], dtype=np.float64)),
|
||||
("Near-singular 2x2", np.array([[1,0],[0,1e-6]], dtype=np.float64)),
|
||||
("Orthogonal 3x3", np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],
|
||||
[np.sin(np.pi/4), np.cos(np.pi/4), 0],
|
||||
[0, 0, 1]], dtype=np.float64)),
|
||||
("Identity 3x3", np.eye(3)),
|
||||
("Zero 3x3", np.zeros((3,3))),
|
||||
("Col vector 2x1", np.array([[3],[4]], dtype=np.float64)),
|
||||
("Row vector 1x2", np.array([[3,4]], dtype=np.float64)),
|
||||
# Large-size instantiation cases (N > 5). Literals MUST match the
|
||||
# C++ test matrices in unit-tests/matrix-tests.cpp exactly, and the
|
||||
# C++ references use float32 inputs: cast to float32 before svd().
|
||||
("Tall 7x5", np.array([
|
||||
[-0.7528, 2.7043, 1.392, 0.592, -2.0639],
|
||||
[-2.064, -2.6515, 2.1971, 0.6067, 1.2484],
|
||||
[-2.8765, 2.8195, 1.9947, -1.726, -1.9091],
|
||||
[-1.8996, -1.1745, 0.1485, -0.4083, -1.2526],
|
||||
[0.6711, -2.163, -1.2471, -0.8018, -0.2636],
|
||||
[1.7111, -1.802, 0.0854, 0.5545, -2.7213],
|
||||
[0.6453, -1.9769, -2.6097, 2.6933, 2.7938]], dtype=np.float32)),
|
||||
("Square 6x6", np.array([
|
||||
[1.2336, -0.7815, -1.6093, 0.7369, -0.2394, -1.5118],
|
||||
[-0.0193, -1.8624, 1.6373, -0.9649, 0.6501, -0.7532],
|
||||
[0.0803, 0.1868, -1.2606, 1.8783, 1.1005, 1.758],
|
||||
[1.5793, 0.3916, 1.6875, -1.646, -1.2161, -1.8191],
|
||||
[-0.6987, -0.4453, -0.9146, 1.315, -0.573, -0.8763],
|
||||
[0.1708, -1.4363, 1.2088, -1.7018, 1.089, 1.9475]], dtype=np.float32)),
|
||||
("Wide 5x8", np.array([
|
||||
[-1.5064, -2.4724, 1.5773, 1.0343, 1.145, 1.3564, -2.1298, -0.7077],
|
||||
[-1.9207, 1.8155, 0.6165, -0.8455, -2.1822, -0.9451, -0.8741, 1.148],
|
||||
[0.6878, 1.9361, -0.1389, -1.902, 1.0662, 1.3039, 0.3064, 1.3548],
|
||||
[-0.031, 0.1137, -0.3623, -2.3729, -1.9605, -2.3429, 0.6821, -0.9282],
|
||||
[0.0429, 2.0378, -1.2535, -0.4481, 1.2778, -1.356, -2.1151, -1.0512]], dtype=np.float32)),
|
||||
("Tall 6x4 rank-def", np.array([
|
||||
[-0.086904, 1.410225, 1.308323, 2.234762],
|
||||
[0.022123, 0.896751, 0.324176, 0.773607],
|
||||
[-0.473015, 1.555111, 0.290059, 1.157726],
|
||||
[-0.78371, 1.398884, -1.930606, -1.548717],
|
||||
[0.201518, -0.626835, 0.976596, 0.875294],
|
||||
[-1.24206, 1.60595, -3.078089, -2.73695]], dtype=np.float32)),
|
||||
]
|
||||
|
||||
for name, A in test_matrices:
|
||||
U, s, Vt = svd(A, full_matrices=False)
|
||||
|
||||
print(f"\n{name}: shape={A.shape}")
|
||||
print(f" Singular values: {[round(float(x), 12) for x in s]}")
|
||||
print(f" U:\n{np.array2string(U, precision=6, floatmode='maxprec_equal')}")
|
||||
print(f" Vt:\n{np.array2string(Vt, precision=6, floatmode='maxprec_equal')}")
|
||||
recon_err = np.linalg.norm(A - U @ np.diag(s) @ Vt, 'fro')
|
||||
print(f" Reconstruction error: {recon_err:.2e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Test 8: Implicit QR Iteration on Bidiagonal
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST 8: implicitQRIteration")
|
||||
print("=" * 70)
|
||||
|
||||
qr_tests = [
|
||||
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
|
||||
("3x3 diag", np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
|
||||
]
|
||||
|
||||
for name, A in qr_tests:
|
||||
B, QL, QR = householder_bidiagonalization(A)
|
||||
Sigma, QR_final = implicit_qr_iteration(B.copy(), QR.copy())
|
||||
|
||||
print(f"\n{name}:")
|
||||
print(f" Bidiagonal B:\n{B}")
|
||||
print(f" After QR iteration (Sigma):\n{Sigma}")
|
||||
print(f" Diagonal entries: {[round(float(Sigma[i,i]), 10) for i in range(min(Sigma.shape))]}")
|
||||
|
||||
# Verify: QL @ Sigma @ QR_final^T ≈ A
|
||||
recon = QL @ Sigma @ QR_final.T
|
||||
err = np.linalg.norm(recon - A, 'fro')
|
||||
print(f" ||QL @ Sigma @ QR^T - A||_F = {err:.2e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JSON output for easy import into C++ tests
|
||||
# ------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print("JSON OUTPUT (for easy C++ integration)")
|
||||
print("=" * 70)
|
||||
|
||||
json_data = {}
|
||||
|
||||
# Householder test vectors
|
||||
hh_tests = {}
|
||||
for name, vec in test_vectors:
|
||||
v, alpha = compute_householder(vec)
|
||||
x = np.array(vec)
|
||||
Hx = x - 2 * np.dot(v, x) * v
|
||||
hh_tests[name] = {
|
||||
"input": [float(xi) for xi in x],
|
||||
"norm": float(np.linalg.norm(x)),
|
||||
"alpha": float(alpha),
|
||||
"v_normalized": [round(float(vi), 12) for vi in v],
|
||||
"Hx": [round(float(xi), 12) for xi in Hx],
|
||||
}
|
||||
json_data["householder_vectors"] = hh_tests
|
||||
|
||||
# Full SVD reference values
|
||||
svd_tests = {}
|
||||
for name, A in test_matrices:
|
||||
U, s, Vt = svd(A, full_matrices=False)
|
||||
svd_tests[name] = {
|
||||
"shape": list(A.shape),
|
||||
"singular_values": [round(float(x), 12) for x in s],
|
||||
"U": [[round(float(U[i,j]), 8) for j in range(U.shape[1])] for i in range(U.shape[0])],
|
||||
"Vt": [[round(float(Vt[i,j]), 8) for j in range(Vt.shape[1])] for i in range(Vt.shape[0])],
|
||||
}
|
||||
json_data["svd_reference"] = svd_tests
|
||||
|
||||
print(json.dumps(json_data, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user