Fix SVD pipeline bugs; extract and unit-test bidiagonal block solvers
- Snapshot bidiagonal block before solving (W overwrite corrupted Ublock) - Wide-matrix QL row extent fix (rowsQL = n when transposed) - Vt output copy bound j<columns (was OOB write clobbering caller sigma) - Rank-deficient blocks: residual singular values sigma_i = ||B*v_i|| instead of sqrt(eigs of B*B) (condition-number squaring); Gram-Schmidt orthonormalization of U columns with rank-deficiency completion - JacobiEigenSymmetric: return signed eigenvalues (removed fabsf) - Fix 28 WithinRel(0.0f,.) matcher misuse -> WithinAbs in matrix-tests - New unit tests: SolveBidiagonalBlock2x2 (7 scipy-referenced cases), JacobiEigenSymmetric (3 cases), DeflateBidiagonal/BidiagonalIsDiagonal
This commit is contained in:
@@ -1406,3 +1406,232 @@ TEST_CASE("SVD Phase 1: Bidiagonalize reconstruction property", "[Matrix][SVD]")
|
||||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(1e-3f, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST: SolveBidiagonalBlock2x2 — 2×2 upper-bidiagonal block SVD
|
||||
// ============================================================================
|
||||
// Reference singular values generated with scipy.linalg.svd for
|
||||
// B = [[a, b], [0, d]].
|
||||
TEST_CASE("SVD Building Block: SolveBidiagonalBlock2x2", "[Matrix][SVD]") {
|
||||
struct Case2x2 {
|
||||
float a, b, d;
|
||||
float refSigma[2];
|
||||
};
|
||||
const Case2x2 cases[] = {
|
||||
{2.5f, -1.3f, 0.8f, {2.84346151f, 0.70336806f}},
|
||||
{3.0f, 0.0f, 1.0f, {3.0f, 1.0f}},
|
||||
{1.0f, 2.0f, 0.0f, {2.23606798f, 0.0f}},
|
||||
{-1.5f, 0.7f, -2.2f, {2.37779179f, 1.38784229f}},
|
||||
{1.0f, 1e-4f, 0.0f, {1.0f, 0.0f}},
|
||||
{-1.770486f, 0.281880f, 0.208573f, {1.79308863f, 0.20594385f}},
|
||||
{0.866025f, 1.0f, 0.5f, {1.37890797f, 0.31402567f}},
|
||||
};
|
||||
|
||||
for (const auto &tc : cases) {
|
||||
float Ublock[2][2] = {{0}}, Vblock[2][2] = {{0}}, sigma[2] = {0};
|
||||
SVD::SolveBidiagonalBlock2x2(tc.a, tc.b, tc.d, Ublock, Vblock, sigma);
|
||||
|
||||
// 1. Singular values match scipy
|
||||
REQUIRE_THAT(sigma[0],
|
||||
Catch::Matchers::WithinRel(tc.refSigma[0], 1e-3f));
|
||||
if (tc.refSigma[1] > 0.0f) {
|
||||
REQUIRE_THAT(sigma[1],
|
||||
Catch::Matchers::WithinRel(tc.refSigma[1], 1e-3f));
|
||||
} else {
|
||||
REQUIRE(sigma[1] < 1e-3f);
|
||||
}
|
||||
REQUIRE(sigma[0] >= sigma[1]);
|
||||
|
||||
// 2. Ublock and Vblock are orthogonal (MᵀM = I)
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = i; j < 2; j++) {
|
||||
float dotU = Ublock[0][i] * Ublock[0][j] + Ublock[1][i] * Ublock[1][j];
|
||||
float dotV = Vblock[0][i] * Vblock[0][j] + Vblock[1][i] * Vblock[1][j];
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
REQUIRE_THAT(dotU, Catch::Matchers::WithinAbs(expected, 1e-3f));
|
||||
REQUIRE_THAT(dotV, Catch::Matchers::WithinAbs(expected, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Ublock · diag(sigma) · Vblockᵀ reproduces B = [[a,b],[0,d]]
|
||||
// (C[i][j] = sum_k U[i][k] * sigma[k] * V[j][k])
|
||||
float C[2][2] = {{0}, {0}};
|
||||
for (int i = 0; i < 2; i++)
|
||||
for (int j = 0; j < 2; j++)
|
||||
for (int k = 0; k < 2; k++)
|
||||
C[i][j] += Ublock[i][k] * sigma[k] * Vblock[j][k];
|
||||
REQUIRE_THAT(C[0][0], Catch::Matchers::WithinAbs(tc.a, 1e-2f));
|
||||
REQUIRE_THAT(C[0][1], Catch::Matchers::WithinAbs(tc.b, 1e-2f));
|
||||
REQUIRE_THAT(C[1][0], Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||
REQUIRE_THAT(C[1][1], Catch::Matchers::WithinAbs(tc.d, 1e-2f));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST: JacobiEigenSymmetric — cyclic Jacobi eigenvalue decomposition
|
||||
// ============================================================================
|
||||
// Reference eigenvalues generated with scipy.linalg.eigvalsh (desc).
|
||||
TEST_CASE("SVD Building Block: JacobiEigenSymmetric", "[Matrix][SVD]") {
|
||||
struct CaseJac {
|
||||
float S[5][5];
|
||||
uint8_t n;
|
||||
float refEig[5];
|
||||
};
|
||||
|
||||
// (i) T = BᵀB from a real bidiagonalization (3×3)
|
||||
float T3[5][5] = {
|
||||
{65.999993f, -124.470864f, 0.0f, 0.0f, 0.0f},
|
||||
{-124.470864f, 237.877008f, -0.499065f, 0.0f, 0.0f},
|
||||
{0.0f, -0.499065f, 0.122959f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
|
||||
};
|
||||
// (ii) random-looking 3×3 symmetric (seed 42)
|
||||
float S3[5][5] = {
|
||||
{0.304717f, -0.04971f, 0.439146f, 0.0f, 0.0f},
|
||||
{-0.04971f, -1.951035f, -0.809211f, 0.0f, 0.0f},
|
||||
{0.439146f, -0.809211f, -0.016801f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
|
||||
};
|
||||
// (iii) random-looking 4×4 symmetric (seed 42)
|
||||
float S4[5][5] = {
|
||||
{-0.853044f, 1.00332f, -0.090545f, -0.307449f, 0.0f},
|
||||
{1.00332f, 0.467509f, 0.009579f, 0.795646f, 0.0f},
|
||||
{-0.090545f, 0.009579f, -0.049926f, -0.169696f, 0.0f},
|
||||
{-0.307449f, 0.795646f, -0.169696f, -0.428328f, 0.0f},
|
||||
{0.0f, 0.0f, 0.0f, 0.0f, 0.0f},
|
||||
};
|
||||
|
||||
float refs[3][5] = {
|
||||
{303.195295f, 0.765908223f, 0.0387564408f, 0, 0},
|
||||
{0.7227162f, -0.13661881f, -2.24921639f, 0, 0},
|
||||
{1.22127596f, -0.01555681f, -0.31307273f, -1.75643542f, 0},
|
||||
};
|
||||
uint8_t ns[3] = {3, 3, 4};
|
||||
float (*mats[3])[5] = {T3, S3, S4};
|
||||
float maxAbs[3] = {237.877008f, 1.951035f, 1.00332f};
|
||||
|
||||
for (int c = 0; c < 3; c++) {
|
||||
float T[5][5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
for (int j = 0; j < 5; j++)
|
||||
T[i][j] = mats[c][i][j];
|
||||
float S_orig[5][5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
for (int j = 0; j < 5; j++)
|
||||
S_orig[i][j] = mats[c][i][j];
|
||||
|
||||
float evals[5] = {0};
|
||||
float V[5][5] = {{0}};
|
||||
SVD::JacobiEigenSymmetric(T, ns[c], evals, V);
|
||||
|
||||
// 1. Sorted eigenvalues match scipy
|
||||
float sorted[5] = {0};
|
||||
for (int i = 0; i < ns[c]; i++) sorted[i] = evals[i];
|
||||
// Sort descending to match the scipy reference order
|
||||
for (int i = 0; i < ns[c] - 1; i++) {
|
||||
int maxIdx = i;
|
||||
for (int j = i + 1; j < ns[c]; j++)
|
||||
if (sorted[j] > sorted[maxIdx])
|
||||
maxIdx = j;
|
||||
if (maxIdx != i) {
|
||||
float t = sorted[i];
|
||||
sorted[i] = sorted[maxIdx];
|
||||
sorted[maxIdx] = t;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < ns[c]; i++) {
|
||||
if (fabsf(refs[c][i]) > 0.01f) {
|
||||
REQUIRE_THAT(sorted[i],
|
||||
Catch::Matchers::WithinRel(refs[c][i], 1e-3f));
|
||||
} else {
|
||||
REQUIRE_THAT(sorted[i], Catch::Matchers::WithinAbs(refs[c][i], 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. V is orthogonal (VᵀV = I on the n×n part)
|
||||
for (int i = 0; i < ns[c]; i++) {
|
||||
for (int j = i; j < ns[c]; j++) {
|
||||
float dot = 0.0f;
|
||||
for (int k = 0; k < ns[c]; k++) dot += V[k][i] * V[k][j];
|
||||
float expected = (i == j) ? 1.0f : 0.0f;
|
||||
REQUIRE_THAT(dot, Catch::Matchers::WithinAbs(expected, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Residual ‖S_orig·V − V·diag(evals)‖ small
|
||||
// (col i of S_orig·V must equal evals_i · col i of V)
|
||||
float residual = 0.0f;
|
||||
for (int i = 0; i < ns[c]; i++) {
|
||||
for (int r = 0; r < ns[c]; r++) {
|
||||
float Sv = 0.0f;
|
||||
for (int k = 0; k < ns[c]; k++) Sv += S_orig[r][k] * V[k][i];
|
||||
float diff = Sv - evals[i] * V[r][i];
|
||||
residual += diff * diff;
|
||||
}
|
||||
}
|
||||
residual = sqrtf(residual);
|
||||
REQUIRE_THAT(residual,
|
||||
Catch::Matchers::WithinAbs(0.0f,
|
||||
1e-2f * maxAbs[c]));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TEST: DeflateBidiagonal / BidiagonalIsDiagonal
|
||||
// ============================================================================
|
||||
TEST_CASE("SVD Building Block: DeflateBidiagonal and BidiagonalIsDiagonal",
|
||||
"[Matrix][SVD]") {
|
||||
float tol = 1e-8f;
|
||||
|
||||
// IsDiagonal: true on a diagonal matrix
|
||||
{
|
||||
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};
|
||||
REQUIRE(SVD::BidiagonalIsDiagonal(W, 5, tol));
|
||||
}
|
||||
|
||||
// IsDiagonal: false when a superdiagonal is significant
|
||||
{
|
||||
Matrix<5, 5> W{10.0f, 1e-3f, 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};
|
||||
REQUIRE_FALSE(SVD::BidiagonalIsDiagonal(W, 5, tol));
|
||||
}
|
||||
|
||||
// Deflate: small superdiagonals zeroed, significant ones kept
|
||||
{
|
||||
Matrix<5, 5> W{1.0f, 0.5f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 2.0f, 1e-9f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 3.0f, 0.3f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 4.0f, 1e-12f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f, 5.0f};
|
||||
SVD::DeflateBidiagonal(W, 5, tol);
|
||||
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(0.5f, 1e-6f));
|
||||
REQUIRE(W.Get(1, 2) == 0.0f);
|
||||
REQUIRE_THAT(W.Get(2, 3), Catch::Matchers::WithinAbs(0.3f, 1e-6f));
|
||||
REQUIRE(W.Get(3, 4) == 0.0f);
|
||||
// Diagonal untouched
|
||||
REQUIRE_THAT(W.Get(0, 0), Catch::Matchers::WithinAbs(1.0f, 1e-6f));
|
||||
REQUIRE_THAT(W.Get(4, 4), Catch::Matchers::WithinAbs(5.0f, 1e-6f));
|
||||
// NOT fully diagonal: significant superdiagonals (0.5, 0.3) remain
|
||||
REQUIRE_FALSE(SVD::BidiagonalIsDiagonal(W, 5, tol));
|
||||
}
|
||||
|
||||
// Deflate on an already-diagonal-ish matrix makes IsDiagonal true
|
||||
{
|
||||
Matrix<5, 5> W{1.0f, 1e-9f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 2.0f, 1e-11f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 3.0f, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 4.0f, 1e-10f,
|
||||
0.0f, 0.0f, 0.0f, 0.0f, 5.0f};
|
||||
SVD::DeflateBidiagonal(W, 5, tol);
|
||||
REQUIRE(SVD::BidiagonalIsDiagonal(W, 5, tol));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user