Add 5x5 SVD end-to-end tests (scipy references)
- Full-rank random 5x5 (cond ~11.5) - Rank-deficient 5x5 (exact rank 3, noise-level tail sigmas) - Wide dynamic range 5x5 (cond ~9000, diagonal decay 50 -> 1e-3) - Symmetric indefinite 5x5 (sigma = |eigenvalues|)
This commit is contained in:
@@ -916,6 +916,132 @@ TEST_CASE("SVD: 5×5 Symmetric Tridiagonal", "Matrix") {
|
|||||||
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD: 5×5 Full-Rank Random", "Matrix") {
|
||||||
|
// Reference: scipy.linalg.svd, np.random.default_rng(7).standard_normal((5,5))
|
||||||
|
// cond ≈ 11.5
|
||||||
|
// σ = [3.04651784, 2.22681732, 1.84290662, 1.02101969, 0.264826749]
|
||||||
|
Matrix<5, 5> A{0.00123015f, 0.298746f, -0.274138f, -0.890592f, -0.454671f,
|
||||||
|
-0.991647f, 0.0601436f, 1.34022f, -0.492207f, -0.620475f,
|
||||||
|
0.489842f, 0.356887f, 0.105414f, -0.930468f, -0.0292518f,
|
||||||
|
0.695303f, -1.34421f, -0.457616f, -1.90122f, -1.28954f,
|
||||||
|
-1.84174f, -0.235091f, -1.26745f, 0.271264f, 0.156751f};
|
||||||
|
Matrix<5, 5> U{}, Vt{};
|
||||||
|
Matrix<5, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(3.04651784f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.22681732f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.84290662f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(3, 0),
|
||||||
|
Catch::Matchers::WithinRel(1.02101969f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(0.264826749f, 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: 5×5 Rank-Deficient (rank 3)", "Matrix") {
|
||||||
|
// Reference: scipy.linalg.svd of rng.standard_normal((5,3)) @
|
||||||
|
// rng.standard_normal((3,5)) — exactly rank 3
|
||||||
|
// σ = [7.29829771, 2.76487864, 1.57392325, ~1e-16, ~1e-16]
|
||||||
|
Matrix<5, 5> A{3.46252f, -1.52873f, -0.111526f, 1.28954f, -5.18688f,
|
||||||
|
-1.34702f, 1.92936f, -0.0410797f, -0.958791f, 0.449623f,
|
||||||
|
0.844592f, 0.0986352f, 0.408213f, 0.124867f, -2.45393f,
|
||||||
|
1.34177f, -0.587312f, -1.39847f, 0.580032f, -0.167692f,
|
||||||
|
0.915462f, -0.311165f, -1.16141f, 0.377283f, 0.055373f};
|
||||||
|
Matrix<5, 5> U{}, Vt{};
|
||||||
|
Matrix<5, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(7.29829771f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.76487864f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.57392325f, 1e-4f));
|
||||||
|
// The two rank-deficient singular values must be at noise level
|
||||||
|
REQUIRE(sigma.Get(3, 0) < 1e-3f);
|
||||||
|
REQUIRE(sigma.Get(4, 0) < 1e-3f);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Rank-3 matrix: the top-3 SVD terms must reproduce A
|
||||||
|
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||||
|
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 5e-3f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD: 5×5 Wide Dynamic Range (cond ≈ 9000)", "Matrix") {
|
||||||
|
// Symmetric banded, diagonal decays 50 → 1e-3, off-diagonals 3 → 0.01
|
||||||
|
// Reference: scipy.linalg.svd
|
||||||
|
// σ = [50.1496395, 10.1719501, 1.02846061, 0.0207253626, 0.00557535757]
|
||||||
|
Matrix<5, 5> A{50.0f, 3.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
-3.0f, 10.0f, 0.5f, 0.0f, 0.0f,
|
||||||
|
0.0f, -0.5f, 1.0f, 0.08f, 0.0f,
|
||||||
|
0.0f, 0.0f, -0.08f, 0.01f, 0.01f,
|
||||||
|
0.0f, 0.0f, 0.0f, -0.01f, 0.001f};
|
||||||
|
Matrix<5, 5> U{}, Vt{};
|
||||||
|
Matrix<5, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0),
|
||||||
|
Catch::Matchers::WithinRel(50.1496395f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0),
|
||||||
|
Catch::Matchers::WithinRel(10.1719501f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0),
|
||||||
|
Catch::Matchers::WithinRel(1.02846061f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(3, 0),
|
||||||
|
Catch::Matchers::WithinRel(0.0207253626f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(4, 0),
|
||||||
|
Catch::Matchers::WithinRel(0.00557535757f, 1e-3f));
|
||||||
|
|
||||||
|
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);
|
||||||
|
// Relative to the largest entry (‖A‖F ≈ 50.1)
|
||||||
|
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 5e-2f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD: 5×5 Symmetric Indefinite", "Matrix") {
|
||||||
|
// Symmetric with negative eigenvalues — σ must equal |eigenvalues|
|
||||||
|
// Reference: scipy.linalg.svd
|
||||||
|
// σ = [4.70141723, 3.76392521, 2.73426097, 1.15773083, 0.642665756]
|
||||||
|
Matrix<5, 5> A{2.0f, -1.0f, 0.0f, 0.0f, 0.5f,
|
||||||
|
-1.0f, 2.0f, -1.0f, 0.0f, 0.0f,
|
||||||
|
0.0f, -1.0f, 3.0f, -1.0f, 0.0f,
|
||||||
|
0.0f, 0.0f, -1.0f, 2.0f, -1.0f,
|
||||||
|
0.5f, 0.0f, 0.0f, -1.0f, 4.0f};
|
||||||
|
Matrix<5, 5> U{}, Vt{};
|
||||||
|
Matrix<5, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0),
|
||||||
|
Catch::Matchers::WithinRel(4.70141723f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0),
|
||||||
|
Catch::Matchers::WithinRel(3.76392521f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0),
|
||||||
|
Catch::Matchers::WithinRel(2.73426097f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(3, 0),
|
||||||
|
Catch::Matchers::WithinRel(1.15773083f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(4, 0),
|
||||||
|
Catch::Matchers::WithinRel(0.642665756f, 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: Non-Square with Negative Values (2×3)", "Matrix") {
|
TEST_CASE("SVD: Non-Square with Negative Values (2×3)", "Matrix") {
|
||||||
// Reference: scipy.linalg.svd([[0.5,-0.3,0.8],[-0.2,0.7,0.1]])
|
// Reference: scipy.linalg.svd([[0.5,-0.3,0.8],[-0.2,0.7,0.1]])
|
||||||
// σ = [1.0384009867, 0.6646227432]
|
// σ = [1.0384009867, 0.6646227432]
|
||||||
|
|||||||
Reference in New Issue
Block a user