Templatize SVD on N: support any Matrix<R,C> with stack-only buffers
Replace the fixed 5x5 SVD implementation with template <uint8_t N> building blocks over Matrix<N,N> working buffers (N = max(rows, cols)), removing the 5x5 size limit. Public API (SVD::SVD) is unchanged and the whole path stays heap-free: peak stack is ~11*N^2 floats, budgeted in the SVD.hpp header doc. Fixes found while porting/validating the templated rewrite: - QL.Identity() was a no-op (static factory returns by value); init the Householder accumulators with an explicit diagonal loop - restore the QL column sign-flip in ExtractAndSortSingularValues for negative unsolved W diagonal entries (Householder sign flips) - T = B^T B tridiagonal formula: T[i][i] = d[i]^2 + e[i-1]^2 only (e[i] contributes to T[i+1][i+1], not T[i][i]) - wide-matrix Vt assembly: Vt = QL^T must be filled over the full m x m (m = columns of A), not just the top n x n Tests: - matrix-tests: add large-size instantiation cases beyond the old limit (tall 7x5 N=7, square 6x6 N=6, wide 5x8 N=8 transpose path with full orthogonal 8x8 Vt, tall 6x4 near rank-deficient N=6 deflation path), all checked against numpy/scipy float32 references - svd-build-blocks-tests: adapt Jacobi test to the Matrix<N,N> interface - svd-reference-values.py: add the four new reference matrices
This commit is contained in:
+138
-1
@@ -1160,4 +1160,141 @@ TEST_CASE("SVD: 1×2 Row Vector", "Matrix") {
|
||||
|
||||
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||||
}
|
||||
}
|
||||
// ============================================================================
|
||||
// SVD Tests — Large-Size Instantiations (N > 5)
|
||||
//
|
||||
// The SVD is templated on N = max(rows, cols) with stack-only buffers, so
|
||||
// these cases exercise instantiations beyond the old 5×5 hard limit:
|
||||
// 7×5 (N=7, tall), 6×6 (N=6, square), 5×8 (N=8, wide/transpose path),
|
||||
// 6×4 (N=6, tall, near rank-deficiency → deflation path).
|
||||
// Reference singular values: scipy.linalg.svd.
|
||||
// ============================================================================
|
||||
|
||||
TEST_CASE("SVD: Tall 7×5 Matrix (N=7)", "Matrix") {
|
||||
// Reference: scipy.linalg.svd
|
||||
// σ = [7.9180769443, 4.6593687008, 4.2921645616, 2.6009010840, 1.9842770351]
|
||||
Matrix<7, 5> A{-0.7528f, 2.7043f, 1.392f, 0.592f, -2.0639f,
|
||||
-2.064f, -2.6515f, 2.1971f, 0.6067f, 1.2484f,
|
||||
-2.8765f, 2.8195f, 1.9947f, -1.726f, -1.9091f,
|
||||
-1.8996f, -1.1745f, 0.1485f, -0.4083f, -1.2526f,
|
||||
0.6711f, -2.163f, -1.2471f, -0.8018f, -0.2636f,
|
||||
1.7111f, -1.802f, 0.0854f, 0.5545f, -2.7213f,
|
||||
0.6453f, -1.9769f, -2.6097f, 2.6933f, 2.7938f};
|
||||
Matrix<7, 5> U{};
|
||||
Matrix<5, 5> Vt{};
|
||||
Matrix<5, 1> sigma{};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(7.9180769443f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.6593687008f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(4.2921645616f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(2.6009010840f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(1.9842770351f, 1e-4f));
|
||||
|
||||
REQUIRE(isSortedDescending(sigma, 5));
|
||||
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("SVD: Square 6×6 Matrix (N=6)", "Matrix") {
|
||||
// Reference: scipy.linalg.svd (float32 inputs)
|
||||
// σ = [5.018912792, 4.244967461, 2.505512476,
|
||||
// 1.838801861, 0.9111995101, 0.4580149353]
|
||||
Matrix<6, 6> A{1.2336f, -0.7815f, -1.6093f, 0.7369f, -0.2394f, -1.5118f,
|
||||
-0.0193f, -1.8624f, 1.6373f, -0.9649f, 0.6501f, -0.7532f,
|
||||
0.0803f, 0.1868f, -1.2606f, 1.8783f,
|
||||
1.1005f, 1.758f, 1.5793f, 0.3916f, 1.6875f, -1.646f,
|
||||
-1.2161f, -1.8191f, -0.6987f, -0.4453f, -0.9146f, 1.315f,
|
||||
-0.573f, -0.8763f, 0.1708f, -1.4363f, 1.2088f, -1.7018f,
|
||||
1.089f, 1.9475f};
|
||||
Matrix<6, 6> U{}, Vt{};
|
||||
Matrix<6, 1> sigma{};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.018912792f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.244967461f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.505512476f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(1.838801861f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(0.9111995101f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(5, 0), Catch::Matchers::WithinRel(0.4580149353f, 1e-4f));
|
||||
|
||||
REQUIRE(isSortedDescending(sigma, 6));
|
||||
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("SVD: Wide 5×8 Matrix (N=8, transpose path)", "Matrix") {
|
||||
// Reference: scipy.linalg.svd
|
||||
// σ = [5.8027782929, 4.1105282764, 3.7755966048, 3.3208483982, 2.0321410547]
|
||||
//
|
||||
// Wide matrices take the Aᵀ transpose path; Vt must be the FULL 8×8
|
||||
// orthogonal matrix (all 8 rows meaningful), not just the top 5.
|
||||
Matrix<5, 8> A{-1.5064f, -2.4724f, 1.5773f, 1.0343f, 1.145f, 1.3564f, -2.1298f, -0.7077f,
|
||||
-1.9207f, 1.8155f, 0.6165f, -0.8455f, -2.1822f, -0.9451f, -0.8741f, 1.148f,
|
||||
0.6878f, 1.9361f, -0.1389f, -1.902f, 1.0662f, 1.3039f, 0.3064f, 1.3548f,
|
||||
-0.031f, 0.1137f, -0.3623f, -2.3729f, -1.9605f, -2.3429f, 0.6821f, -0.9282f,
|
||||
0.0429f, 2.0378f, -1.2535f, -0.4481f, 1.2778f, -1.356f, -2.1151f, -1.0512f};
|
||||
Matrix<5, 8> U{};
|
||||
Matrix<8, 8> Vt{};
|
||||
Matrix<8, 1> sigma{};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.8027782929f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.1105282764f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(3.7755966048f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(3.3208483982f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(2.0321410547f, 1e-4f));
|
||||
// Remaining singular values must be at noise level
|
||||
REQUIRE(sigma.Get(5, 0) < 1e-3f);
|
||||
REQUIRE(sigma.Get(6, 0) < 1e-3f);
|
||||
REQUIRE(sigma.Get(7, 0) < 1e-3f);
|
||||
|
||||
REQUIRE(isSortedDescending(sigma, 8));
|
||||
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("SVD: Tall 6×4 Near Rank-Deficient (N=6, deflation path)", "Matrix") {
|
||||
// Reference: scipy.linalg.svd
|
||||
// σ = [5.9434060901, 3.2857910666, 0.3066158795, 6.48e-07]
|
||||
//
|
||||
// σ₄ ≈ 6.5e-7 forces the deflation logic to zero the last
|
||||
// superdiagonal and isolate the trailing 1×1 block.
|
||||
Matrix<6, 4> A{-0.086904f, 1.410225f, 1.308323f, 2.234762f,
|
||||
0.022123f, 0.896751f, 0.324176f, 0.773607f,
|
||||
-0.473015f, 1.555111f, 0.290059f, 1.157726f,
|
||||
-0.78371f, 1.398884f, -1.930606f, -1.548717f,
|
||||
0.201518f, -0.626835f, 0.976596f, 0.875294f,
|
||||
-1.24206f, 1.60595f, -3.078089f, -2.73695f};
|
||||
Matrix<6, 4> U{};
|
||||
Matrix<4, 4> Vt{};
|
||||
Matrix<4, 1> sigma{};
|
||||
|
||||
SVD::SVD(A, U, sigma, Vt);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.9434060901f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(3.2857910666f, 1e-4f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(0.3066158795f, 1e-4f));
|
||||
// Fourth singular value is at noise level (matrix is ~rank 3)
|
||||
REQUIRE(sigma.Get(3, 0) < 1e-4f);
|
||||
|
||||
REQUIRE(isSortedDescending(sigma, 4));
|
||||
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
|
||||
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user