Working on an SVD implimentation

This commit is contained in:
2026-08-14 09:19:56 -04:00
parent a49e357f4c
commit 5600b05b09
8 changed files with 2640 additions and 0 deletions
+396
View File
@@ -4,6 +4,7 @@
// include the module you're going to test next
#include "Matrix.hpp"
#include "SVD.hpp"
// any other libraries
#include <array>
@@ -637,4 +638,399 @@ TEST_CASE("Eigenvalues and Vectors", "Matrix") {
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(0.0f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(16.1168f, 1e-4f));
}
}
// ============================================================================
// SVD Tests — Reference values computed via scipy.linalg.svd (Python)
// ============================================================================
/**
* @brief Helper: compute Frobenius norm of a matrix.
*/
template <uint8_t rows, uint8_t columns>
static float frobeniusNorm(const Matrix<rows, columns> &M) {
float sum = 0;
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < columns; j++) {
float v = M.Get(i, j);
sum += v * v;
}
}
return sqrtf(sum);
}
/**
* @brief Helper: compute reconstruction error ||A - UΣVᵀ||_F.
*
* Verifies the fundamental SVD identity A = U × diag(σ) × Vᵀ.
* For non-square matrices, only the first min(rows,cols) singular values
* contribute to the reconstruction.
*/
template <uint8_t rows, uint8_t columns>
static float svdReconstructionError(const Matrix<rows, columns> &A,
const Matrix<rows, columns> &U,
const Matrix<columns, 1> &sigma,
const Matrix<columns, columns> &Vt) {
// Compute U × diag(σ): only first min(rows,cols) columns of U are used
constexpr uint8_t k = (rows < columns) ? rows : columns;
Matrix<rows, columns> USigma{0};
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < k; j++) {
USigma[i][j] = U.Get(i, j) * sigma.Get(j, 0);
}
}
// Compute (UΣ) × Vᵀ: only first k rows of Vt are used
Matrix<rows, columns> UVt{0};
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < columns; j++) {
float sum = 0;
for (uint8_t p = 0; p < k; p++) {
sum += USigma[i][p] * Vt.Get(p, j);
}
UVt[i][j] = sum;
}
}
// Compute ||A - UVᵀ||_F
Matrix<rows, columns> diff{0};
A.Sub(UVt, diff);
return frobeniusNorm(diff);
}
/**
* @brief Helper: check orthogonality of the first k columns of M.
* Verifies M[:,0:k]ᵀ × M[:,0:k] ≈ I_k.
*/
template <uint8_t rows, uint8_t columns>
static float orthogonalityError(const Matrix<rows, columns> &M) {
constexpr uint8_t k = (rows < columns) ? rows : columns;
// Compute Mᵀ × M (should be I_k in top-left)
Matrix<columns, rows> Mt = M.Transpose();
Matrix<columns, columns> MtM{0};
Mt.Mult(M, MtM);
float err = 0;
for (uint8_t i = 0; i < k; i++) {
for (uint8_t j = 0; j < k; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
err += (MtM.Get(i, j) - expected) * (MtM.Get(i, j) - expected);
}
}
return sqrtf(err);
}
/**
* @brief Helper: check that singular values are sorted in descending order.
*/
template <uint8_t maxCols>
static bool isSortedDescending(const Matrix<maxCols, 1> &sigma, uint8_t count) {
for (uint8_t i = 0; i < count - 1; i++) {
if (sigma.Get(i + 1, 0) > sigma.Get(i, 0) + 1e-6f) {
return false;
}
}
return true;
}
TEST_CASE("SVD: Simple 2x2 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2],[3,4]])
// σ = [5.4649857042, 0.3659661906]
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// Verify singular values (verified with Python scipy.linalg.svd)
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(5.4649857042f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.3659661906f, 1e-4f));
// Verify descending order
REQUIRE(isSortedDescending(sigma, 2));
// Verify U is orthogonal: UᵀU ≈ I
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinRel(0.0f, 1e-4f));
// Verify Vt is orthogonal: VtVᵀ ≈ I
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinRel(0.0f, 1e-4f));
// Verify reconstruction: A ≈ U Σ Vᵀ
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: Symmetric Positive Definite 2x2", "Matrix") {
// Reference: scipy.linalg.svd([[5,3],[3,5]])
// σ = [8.0, 2.0] (eigenvalues since symmetric PD)
Matrix<2, 2> A{5.0f, 3.0f, 3.0f, 5.0f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(8.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
// For symmetric PD matrices, U ≈ V (up to sign)
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: Full-Rank 3x3 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2,3],[4,5,6],[7,8,10]])
// σ = [17.4125051668, 0.8751613501, 0.1968665211]
Matrix<3, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(17.4125051668f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.8751613501f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0),
Catch::Matchers::WithinRel(0.1968665211f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 3));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinRel(0.0f, 1e-4f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinRel(0.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-3f));
}
TEST_CASE("SVD: Rank-Deficient 3x3 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2,3],[4,5,6],[7,8,9]])
// σ = [16.8481033526, 1.0683695146, ~0] (rank 2)
Matrix<3, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(16.8481033526f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(1.0683695146f, 1e-4f));
// Third singular value should be ~0 (rank deficiency)
REQUIRE(sigma.Get(2, 0) < 1e-3f);
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-3f));
}
TEST_CASE("SVD: Diagonal 3x3 Matrix", "Matrix") {
// For a diagonal matrix, σ = diagonal entries, U = V = I
Matrix<3, 3> A{10.0f, 0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 2.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: Tall Matrix (4×3)", "Matrix") {
// Reference: scipy.linalg.svd with full_matrices=False
// σ = [25.4624074360, 1.2906616758, ~0] (rank 2)
Matrix<4, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f,
7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f};
Matrix<4, 3> U{};
Matrix<3, 3> Vt{}; // Vt is always n×n
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(25.4624074360f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(1.2906616758f, 1e-4f));
REQUIRE(sigma.Get(2, 0) < 1e-3f);
// U should be 4×3 with orthonormal columns
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinRel(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-3f));
}
TEST_CASE("SVD: Wide Matrix (3×5)", "Matrix") {
// Reference: scipy.linalg.svd with full_matrices=False
// σ = [35.1272233336, 2.4653966969, ~0] (rank 2)
Matrix<3, 5> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f};
Matrix<3, 5> U{};
Matrix<5, 5> Vt{}; // Vt is always n×n
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(35.1272233336f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(2.4653966969f, 1e-4f));
REQUIRE(sigma.Get(2, 0) < 1e-3f);
// Vt should be 5×5 with orthonormal rows (first k)
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinRel(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-3f));
}
TEST_CASE("SVD: 5×5 Symmetric Tridiagonal", "Matrix") {
// Reference: scipy.linalg.svd for discrete Laplacian-like matrix
// σ = [3.7320508076, 3.0, 2.0, 1.0, 0.2679491924]
Matrix<5, 5> A{2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 2.0f, -1.0f, 0.0f,
0.0f, 0.0f, -1.0f, 2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f,
2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 2.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(3.7320508076f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(3.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0),
Catch::Matchers::WithinRel(0.2679491924f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinRel(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinRel(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-3f));
}
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]])
// σ = [1.0384009867, 0.6646227432]
Matrix<2, 3> A{0.5f, -0.3f, 0.8f, -0.2f, 0.7f, 0.1f};
Matrix<2, 3> U{};
Matrix<3, 3> Vt{}; // Vt is always n×n
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(1.0384009867f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.6646227432f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: Near-Singular 2×2 Matrix", "Matrix") {
// Condition number ≈ 1e6 — tests numerical stability
// Reference: scipy.linalg.svd([[1,0],[0,1e-6]])
// σ = [1.0, 1e-6]
Matrix<2, 2> A{1.0f, 0.0f, 0.0f, 1e-6f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1e-6f, 1e-2f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-6f));
}
TEST_CASE("SVD: Orthogonal Matrix (3×3)", "Matrix") {
// For an orthogonal matrix, all singular values should be 1.
// Rotation matrix about z-axis by 45°
float c = sqrtf(0.5f); // cos(45°)
float s = sqrtf(0.5f); // sin(45°)
Matrix<3, 3> A{c, -s, 0.0f, s, c, 0.0f, 0.0f, 0.0f, 1.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// All singular values should be 1 for an orthogonal matrix
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: Identity Matrix", "Matrix") {
// For I, σ = [1, 1, 1], U = V = I
Matrix<3, 3> A{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-6f));
}
TEST_CASE("SVD: Zero Matrix", "Matrix") {
// All singular values should be zero
Matrix<3, 3> A{0.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE(sigma.Get(0, 0) < 1e-6f);
REQUIRE(sigma.Get(1, 0) < 1e-6f);
REQUIRE(sigma.Get(2, 0) < 1e-6f);
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-6f));
}
TEST_CASE("SVD: 2×1 Column Vector", "Matrix") {
// For a column vector v, σ = ||v||, U = v/||v|| (with padding)
Matrix<2, 1> A{3.0f, 4.0f};
Matrix<2, 1> U{};
Matrix<1, 1> Vt{}; // Vt is always n×n
Matrix<1, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// σ should be the Euclidean norm: ||[3,4]|| = 5
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}
TEST_CASE("SVD: 1×2 Row Vector", "Matrix") {
// For a row vector vᵀ, σ = ||v||, Vt = v/||v|| (with padding)
Matrix<1, 2> A{3.0f, 4.0f};
Matrix<1, 2> U{};
Matrix<2, 2> Vt{}; // Vt is always n×n
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// σ should be the Euclidean norm: ||[3,4]|| = 5
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinRel(0.0f, 1e-4f));
}