Working on breaking up the steps into manageable chunks

This commit is contained in:
2026-08-17 15:33:33 -04:00
parent 6f91c96de8
commit f12625b41e
5 changed files with 1116 additions and 237 deletions
+381
View File
@@ -1025,3 +1025,384 @@ TEST_CASE("SVD Phase 4: AssembleUAndVt", "[Matrix][SVD]") {
REQUIRE_THAT(Vt.Get(1, 1), Catch::Matchers::WithinRel(-0.6f, 1e-6f));
}
}
// ============================================================================
// TEST 13: Bidiagonalize (Phase 1) - Square matrix
// ===========================================================================
TEST_CASE("SVD Phase 1: Bidiagonalize square matrix", "[Matrix][SVD]") {
// Test case 1: 3×3 matrix
// C++ verified reference:
// W[0] = [-4.123106, -5.335784, 6.548462]
// W[1] = [ 0.000000, 7.037714, -8.107580]
// W[2] = [ 0.000000, 0.000000, 0.620321]
// Note: W[0][2]=6.548462 is NOT zeroed because right HH at k=0 has only 1 element
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
4.0f, 5.0f, 6.0f, 0.0f, 0.0f,
0.0f, 7.0f, 8.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 3, 3, 3, QL, QR);
// Subdiagonal elements should be zero: W[1][0], W[2][0], W[2][1]
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(2, 1), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 2: 2×2 matrix (simplest non-trivial case)
// C++ verified reference:
// W[0] = [-3.162278, -4.427189]
// W[1] = [-0.000000, 0.632456]
{
Matrix<5, 5> W{3.0f, 4.0f, 0.0f, 0.0f, 0.0f,
1.0f, 2.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 2, 2, 2, QL, QR);
// For 2×2, bidiagonal form has no elements to zero out
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 3: Diagonal matrix (no transformations needed)
// C++ verified reference: unchanged
{
Matrix<5, 5> W{10.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 5.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 2.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
Matrix<5, 5> W_orig{10.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 5.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 2.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 3, 3, 3, QL, QR);
// Diagonal matrix may have sign flips but absolute values preserved
float err = 0.0f;
for (uint8_t i = 0; i < 3; i++) {
float diff = fabsf(W.Get(i, i)) - fabsf(W_orig.Get(i, i));
err += diff * diff;
}
REQUIRE_THAT(sqrtf(err), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
// QL and QR may have sign flips but should remain orthogonal
// Check that |QL[i][j]| and |QR[i][j]| match identity pattern
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(fabsf(QL.Get(i, j)), Catch::Matchers::WithinAbs(expected, 1e-6f));
REQUIRE_THAT(fabsf(QR.Get(i, j)), Catch::Matchers::WithinAbs(expected, 1e-6f));
}
}
}
}
// ============================================================================
// TEST 14: Bidiagonalize (Phase 1) — Tall matrix (m > n)
// ===========================================================================
TEST_CASE("SVD Phase 1: Bidiagonalize tall matrix", "[Matrix][SVD]") {
// Test case 1: 4×3 matrix
// C++ verified reference (partial - subdiagonal zeros):
// W[0] = [-4.123106, -5.335784, 6.548462]
// W[1] = [-0.000000, 12.228222, 12.026182]
// W[2] = [ 0.000000, 0.000000, -1.577527]
// W[3] = [ 0.000000, 0.000000, 0.000000]
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
4.0f, 5.0f, 6.0f, 0.0f, 0.0f,
0.0f, 7.0f, 8.0f, 0.0f, 0.0f,
0.0f, 10.0f, 9.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 4, 3, 3, QL, QR);
// Zero below subdiagonal: W[2][0], W[3][0], W[3][1]
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(3, 1), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 2: 3×2 matrix
// C++ verified reference:
// W[0] = [-5.916080, -7.437357]
// W[1] = [-0.000001, 0.828077]
// W[2] = [-0.000000, -0.000000]
{
Matrix<5, 5> W{1.0f, 2.0f, 0.0f, 0.0f, 0.0f,
3.0f, 4.0f, 0.0f, 0.0f, 0.0f,
5.0f, 6.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 3, 2, 2, QL, QR);
// Zero below subdiagonal: W[2][0] ≈ 0
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 3: 5×3 matrix (full 5-row tall)
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
4.0f, 5.0f, 6.0f, 0.0f, 0.0f,
0.0f, 7.0f, 8.0f, 0.0f, 0.0f,
0.0f, 10.0f, 9.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 5, 3, 3, QL, QR);
// Zero below subdiagonal: W[2][0], W[3][0], W[4][0], W[3][1], W[4][1]
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(4, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(3, 1), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(4, 1), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
}
// ============================================================================
// TEST 15: Bidiagonalize (Phase 1) — Wide matrix (m < n)
// ===========================================================================
TEST_CASE("SVD Phase 1: Bidiagonalize wide matrix", "[Matrix][SVD]") {
// Test case 1: 2×4 matrix
// C++ verified reference:
// W[0] = [-5.099020, -6.275717, 11.401754, 0.000000]
// W[1] = [ 0.000000, -0.784465, 2.806586, -0.350823]
// Note: W[1][2]=2.806586 and W[1][3]=-0.350823 are NOT zeroed because
// right HH at k=0 has only 2 elements (cols 2,3), so it zeros col 3 but preserves col 2
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 4.0f, 0.0f,
5.0f, 6.0f, 7.0f, 8.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 2, 4, 2, QL, QR);
// For 2×4: right HH at k=0 has 2 elements (cols 2,3)
// It zeros col 3 but preserves col 2 as the superdiagonal element for row 1
// W[0][3] should be zeroed (above superdiagonal in row 0)
REQUIRE_THAT(W.Get(0, 3), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// W[1][2] and W[1][3] are part of the bidiagonal structure for row 1
// (superdiagonal at col 2, and right HH preserves first element)
REQUIRE_THAT(W.Get(1, 2), !Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 2: 3×5 matrix
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 4.0f, 5.0f,
6.0f, 7.0f, 8.0f, 9.0f, 10.0f,
0.0f, 11.0f, 12.0f, 13.0f, 14.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 3, 5, 3, QL, QR);
// For 3×5: check that subdiagonal elements are zero
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(W.Get(2, 1), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
// Test case 3: 1×3 matrix (row vector)
// C++ verified reference: W[0] = [1.0, 2.0, 3.0] (no transformations needed)
{
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
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::Bidiagonalize(W, 1, 3, 1, QL, QR);
// For 1×3, no transformations needed
REQUIRE_THAT(W.Get(0, 0), Catch::Matchers::WithinAbs(1.0f, 1e-4f));
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(2.0f, 1e-4f));
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(3.0f, 1e-4f));
// Verify orthogonality
REQUIRE(isOrthogonal5(QL));
REQUIRE(isOrthogonal5(QR));
}
}
// ============================================================================
// TEST 16: Bidiagonalize — Reconstruction property
// ===========================================================================
TEST_CASE("SVD Phase 1: Bidiagonalize reconstruction property", "[Matrix][SVD]") {
// Test: QLᵀ · W_original · QR = B (bidiagonal)
// This verifies that the accumulated transformations correctly represent
// the bidiagonalization.
{
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
4.0f, 5.0f, 6.0f, 0.0f, 0.0f,
0.0f, 7.0f, 8.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
Matrix<5, 5> W = W_orig;
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::Bidiagonalize(W, 3, 3, 3, QL, QR);
// Compute QLᵀ · W_orig · QR and verify it equals W (the bidiagonal result)
Matrix<5, 5> Qt = QL.Transpose();
Matrix<5, 5> QtW_orig{0};
Qt.Mult(W_orig, QtW_orig);
Matrix<5, 5> QtW_origQR{0};
QtW_orig.Mult(QR, QtW_origQR);
// The reconstruction should match the bidiagonal result
float err = frobeniusNorm5(W - QtW_origQR);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(1e-3f, 1e-3f));
}
// Test: Tall matrix reconstruction (4×3)
{
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0.0f, 0.0f,
4.0f, 5.0f, 6.0f, 0.0f, 0.0f,
0.0f, 7.0f, 8.0f, 0.0f, 0.0f,
0.0f, 10.0f, 9.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
Matrix<5, 5> W = W_orig;
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::Bidiagonalize(W, 4, 3, 3, QL, QR);
Matrix<5, 5> Qt = QL.Transpose();
Matrix<5, 5> QtW_orig{0};
Qt.Mult(W_orig, QtW_orig);
Matrix<5, 5> QtW_origQR{0};
QtW_orig.Mult(QR, QtW_origQR);
float err = frobeniusNorm5(W - QtW_origQR);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(1e-3f, 1e-3f));
}
// Test: Wide matrix reconstruction (2×4)
{
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 4.0f, 0.0f,
5.0f, 6.0f, 7.0f, 8.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
Matrix<5, 5> W = W_orig;
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::Bidiagonalize(W, 2, 4, 2, QL, QR);
Matrix<5, 5> Qt = QL.Transpose();
Matrix<5, 5> QtW_orig{0};
Qt.Mult(W_orig, QtW_orig);
Matrix<5, 5> QtW_origQR{0};
QtW_orig.Mult(QR, QtW_origQR);
float err = frobeniusNorm5(W - QtW_origQR);
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(1e-3f, 1e-3f));
}
}