Small refactor of SVD implimentation
This commit is contained in:
@@ -783,3 +783,245 @@ TEST_CASE("SVD Building Block: Givens preserves orthogonality",
|
||||
REQUIRE(isOrthogonal5(M_right));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 11: ExtractAndSortSingularValues (Phase 3)
|
||||
// ===========================================================================
|
||||
TEST_CASE("SVD Phase 3: ExtractAndSortSingularValues", "[Matrix][SVD]") {
|
||||
// Test case 1: Diagonal matrix with unordered singular values
|
||||
{
|
||||
Matrix<5, 5> W{0};
|
||||
W[0][0] = 2.0f;
|
||||
W[1][1] = 10.0f;
|
||||
W[2][2] = 5.0f;
|
||||
|
||||
Matrix<5, 1> sigma{0};
|
||||
Matrix<5, 5> QL{0}, QR{0};
|
||||
for (uint8_t i = 0; i < 5; i++) {
|
||||
QL[i][i] = 1.0f;
|
||||
QR[i][i] = 1.0f;
|
||||
}
|
||||
|
||||
SVD::ExtractAndSortSingularValues(W, sigma, 3, QL, QR);
|
||||
|
||||
// Singular values should be sorted descending: [10, 5, 2]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-6f));
|
||||
|
||||
// QL and QR columns should have been swapped to match the sort order
|
||||
// Original: col 0 → σ=2, col 1 → σ=10, col 2 → σ=5
|
||||
// After sort: col 0 has σ=10 (was orig col 1), col 1 has σ=5 (was orig col 2),
|
||||
// col 2 has σ=2 (was orig col 0)
|
||||
// Starting from identity: QL[:,0] = e₀, QL[:,1] = e₁, QL[:,2] = e₂
|
||||
// After swaps: QL[:,0] = e₁, QL[:,1] = e₂, QL[:,2] = e₀
|
||||
REQUIRE_THAT(QL.Get(0, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(1, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(2, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(0, 2), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(1, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(QL.Get(2, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
}
|
||||
|
||||
// Test case 2: Negative diagonal elements (absolute value extraction)
|
||||
{
|
||||
Matrix<5, 5> W{0};
|
||||
W[0][0] = -3.0f;
|
||||
W[1][1] = -7.0f;
|
||||
W[2][2] = 5.0f;
|
||||
|
||||
Matrix<5, 1> sigma{0};
|
||||
Matrix<5, 5> QL{0}, QR{0};
|
||||
for (uint8_t i = 0; i < 5; i++) {
|
||||
QL[i][i] = 1.0f;
|
||||
QR[i][i] = 1.0f;
|
||||
}
|
||||
|
||||
SVD::ExtractAndSortSingularValues(W, sigma, 3, QL, QR);
|
||||
|
||||
// Should extract absolute values and sort: [7, 5, 3]
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(7.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(3.0f, 1e-6f));
|
||||
}
|
||||
|
||||
// Test case 3: Already sorted (no swaps needed)
|
||||
{
|
||||
Matrix<5, 5> W{0};
|
||||
W[0][0] = 9.0f;
|
||||
W[1][1] = 6.0f;
|
||||
W[2][2] = 3.0f;
|
||||
|
||||
Matrix<5, 1> sigma{0};
|
||||
Matrix<5, 5> QL{0}, QR{0};
|
||||
for (uint8_t i = 0; i < 5; i++) {
|
||||
QL[i][i] = 1.0f;
|
||||
QR[i][i] = 1.0f;
|
||||
}
|
||||
|
||||
SVD::ExtractAndSortSingularValues(W, sigma, 3, QL, QR);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(9.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(6.0f, 1e-6f));
|
||||
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(3.0f, 1e-6f));
|
||||
|
||||
// QL and QR should be unchanged (identity)
|
||||
for (uint8_t i = 0; i < 5; i++) {
|
||||
for (uint8_t j = 0; j < 5; j++) {
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
REQUIRE_THAT(QL.Get(i, j), Catch::Matchers::WithinRel(expected, 1e-6f));
|
||||
REQUIRE_THAT(QR.Get(i, j), Catch::Matchers::WithinRel(expected, 1e-6f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test case 4: Single singular value
|
||||
{
|
||||
Matrix<5, 5> W{0};
|
||||
W[0][0] = 42.0f;
|
||||
|
||||
Matrix<5, 1> sigma{0};
|
||||
Matrix<5, 5> QL{0}, QR{0};
|
||||
for (uint8_t i = 0; i < 5; i++) {
|
||||
QL[i][i] = 1.0f;
|
||||
QR[i][i] = 1.0f;
|
||||
}
|
||||
|
||||
SVD::ExtractAndSortSingularValues(W, sigma, 1, QL, QR);
|
||||
|
||||
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(42.0f, 1e-6f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST 12: AssembleUAndVt (Phase 4)
|
||||
// ===========================================================================
|
||||
TEST_CASE("SVD Phase 4: AssembleUAndVt", "[Matrix][SVD]") {
|
||||
// Test case 1: Non-transpose case (m ≥ n) — U from QL, Vt from QRᵀ
|
||||
{
|
||||
uint8_t m = 3, n = 2, p = 2;
|
||||
bool transposeNeeded = false;
|
||||
|
||||
Matrix<5, 5> QL{0};
|
||||
// Make columns orthonormal
|
||||
QL[0][0] = 3.0f / 5.0f;
|
||||
QL[1][0] = 4.0f / 5.0f;
|
||||
QL[2][0] = 0.0f;
|
||||
QL[0][1] = 4.0f / 5.0f;
|
||||
QL[1][1] = -3.0f / 5.0f;
|
||||
QL[2][1] = 0.0f;
|
||||
|
||||
Matrix<5, 5> QR{0};
|
||||
QR[0][0] = 1.0f; // Vt[:,0]ᵀ
|
||||
QR[1][0] = 0.0f;
|
||||
QR[0][1] = 0.0f; // Vt[:,1]ᵀ
|
||||
QR[1][1] = 1.0f;
|
||||
|
||||
Matrix<5, 5> U{0};
|
||||
Matrix<5, 5> Vt{0};
|
||||
|
||||
SVD::AssembleUAndVt(m, n, p, transposeNeeded, QL, QR, U, Vt);
|
||||
|
||||
// U should be QL[:,0:2]
|
||||
REQUIRE_THAT(U.Get(0, 0), Catch::Matchers::WithinRel(3.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(1, 0), Catch::Matchers::WithinRel(4.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(0, 1), Catch::Matchers::WithinRel(4.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(1, 1), Catch::Matchers::WithinRel(-3.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(2, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
|
||||
// Vt should be QR[:,0:2]ᵀ
|
||||
REQUIRE_THAT(Vt.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
|
||||
// Verify U is orthogonal (first p columns)
|
||||
Matrix<5, 5> Ut = U.Transpose();
|
||||
Matrix<5, 5> UtU{0};
|
||||
Ut.Mult(U, UtU);
|
||||
REQUIRE_THAT(UtU.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(UtU.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(UtU.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(UtU.Get(1, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
}
|
||||
|
||||
// Test case 2: Transpose case (m < n) — U from QRᵀ, Vt from QLᵀ
|
||||
{
|
||||
uint8_t m = 2, n = 3, p = 2;
|
||||
bool transposeNeeded = true;
|
||||
|
||||
Matrix<5, 5> QL{0};
|
||||
QL[0][0] = 1.0f; // Vt[:,0]ᵀ
|
||||
QL[1][0] = 0.0f;
|
||||
QL[0][1] = 0.0f; // Vt[:,1]ᵀ
|
||||
QL[1][1] = 1.0f;
|
||||
|
||||
Matrix<5, 5> QR{0};
|
||||
QR[0][0] = 3.0f / 5.0f; // U[:,0]
|
||||
QR[1][0] = 4.0f / 5.0f;
|
||||
QR[2][0] = 0.0f;
|
||||
QR[0][1] = 4.0f / 5.0f;
|
||||
QR[1][1] = -3.0f / 5.0f;
|
||||
QR[2][1] = 0.0f;
|
||||
|
||||
Matrix<5, 5> U{0};
|
||||
Matrix<5, 5> Vt{0};
|
||||
|
||||
SVD::AssembleUAndVt(m, n, p, transposeNeeded, QL, QR, U, Vt);
|
||||
|
||||
// U should be QR[:,0:2]ᵀ → U[i][j] = QR[j][i]
|
||||
REQUIRE_THAT(U.Get(0, 0), Catch::Matchers::WithinRel(3.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(0, 1), Catch::Matchers::WithinRel(4.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(1, 0), Catch::Matchers::WithinRel(4.0f / 5.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(1, 1), Catch::Matchers::WithinRel(-3.0f / 5.0f, 1e-6f));
|
||||
|
||||
// Vt should be QL[:,0:2]ᵀ → Vt[i][j] = QL[j][i]
|
||||
REQUIRE_THAT(Vt.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
|
||||
// Verify U has correct dimensions (m×n = 2×3)
|
||||
REQUIRE(U.Get(0, 2) == 0.0f);
|
||||
REQUIRE(U.Get(1, 2) == 0.0f);
|
||||
|
||||
// Verify Vt is orthogonal (first p rows)
|
||||
Matrix<5, 5> VtVtT{0};
|
||||
Vt.Mult(Vt.Transpose(), VtVtT);
|
||||
REQUIRE_THAT(VtVtT.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(VtVtT.Get(1, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
// Row 2 of Vt is all zeros (p=2 < n=3), so VtVtT[2][2] = 0 is expected
|
||||
}
|
||||
|
||||
// Test case 3: Square matrix (m = n)
|
||||
{
|
||||
uint8_t m = 2, n = 2, p = 2;
|
||||
bool transposeNeeded = false;
|
||||
|
||||
Matrix<5, 5> QL{0};
|
||||
QL[0][0] = 1.0f; QL[1][1] = 1.0f;
|
||||
|
||||
Matrix<5, 5> QR{0};
|
||||
QR[0][0] = 0.6f; QR[0][1] = 0.8f;
|
||||
QR[1][0] = 0.8f; QR[1][1] = -0.6f;
|
||||
|
||||
Matrix<5, 5> U{0};
|
||||
Matrix<5, 5> Vt{0};
|
||||
|
||||
SVD::AssembleUAndVt(m, n, p, transposeNeeded, QL, QR, U, Vt);
|
||||
|
||||
// U = QL[:,0:2]
|
||||
REQUIRE_THAT(U.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(U.Get(1, 1), Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||
|
||||
// Vt = QR[:,0:2]ᵀ
|
||||
REQUIRE_THAT(Vt.Get(0, 0), Catch::Matchers::WithinRel(0.6f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(0, 1), Catch::Matchers::WithinRel(0.8f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 0), Catch::Matchers::WithinRel(0.8f, 1e-6f));
|
||||
REQUIRE_THAT(Vt.Get(1, 1), Catch::Matchers::WithinRel(-0.6f, 1e-6f));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user