1648 lines
56 KiB
C++
1648 lines
56 KiB
C++
// include the unit test framework first
|
||
#include <catch2/catch_test_macros.hpp>
|
||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||
|
||
// include the module you're going to test next
|
||
#include "Matrix.hpp"
|
||
#include "SVD.hpp"
|
||
|
||
// any other libraries
|
||
#include <array>
|
||
#include <cmath>
|
||
#include <iostream>
|
||
|
||
// ============================================================================
|
||
// Helper: Frobenius norm of a 5×5 matrix
|
||
// ============================================================================
|
||
static float frobeniusNorm5(const Matrix<5, 5> &M) {
|
||
float sum = 0.0f;
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
for (uint8_t j = 0; j < 5; j++) {
|
||
float v = M.Get(i, j);
|
||
sum += v * v;
|
||
}
|
||
}
|
||
return sqrtf(sum);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Helper: Check if a matrix is orthogonal (Mᵀ·M ≈ I)
|
||
// ============================================================================
|
||
static bool isOrthogonal5(const Matrix<5, 5> &M, float tol = 1e-6f) {
|
||
Matrix<5, 5> Mt = M.Transpose();
|
||
Matrix<5, 5> MtM{0};
|
||
Mt.Mult(M, MtM);
|
||
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
for (uint8_t j = 0; j < 5; j++) {
|
||
float expected = (i == j) ? 1.0f : 0.0f;
|
||
if (fabsf(MtM.Get(i, j) - expected) > tol) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 1: ComputeHouseholder
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ComputeHouseholder", "[Matrix][SVD]") {
|
||
// Test case: [3, 4] should give alpha = -5 (norm), v normalized
|
||
// Reference: scipy.linalg.householder([3, 4]) → v ≈ [0.894427191,
|
||
// 0.447213596], α = -5
|
||
{
|
||
float x[] = {3.0f, 4.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
// Norm should be 5.0
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||
|
||
// Alpha should be -5 (negative norm)
|
||
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-5.0f, 1e-6f));
|
||
|
||
// v should be normalized: ||v|| ≈ 1
|
||
float vNorm = sqrtf(v[0] * v[0] + v[1] * v[1]);
|
||
REQUIRE_THAT(vNorm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
|
||
// Verify H·x = [alpha, 0]: (I - 2vvᵀ)·x should give [-5, 0]
|
||
float hx0 = x[0] - 2.0f * v[0] * (v[0] * x[0] + v[1] * x[1]);
|
||
float hx1 = x[1] - 2.0f * v[1] * (v[0] * x[0] + v[1] * x[1]);
|
||
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
|
||
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [1, 3]
|
||
// Reference: norm = √10 ≈ 3.16228, alpha = -√10
|
||
{
|
||
float x[] = {1.0f, 3.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(10.0f), 1e-6f));
|
||
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(10.0f), 1e-6f));
|
||
|
||
// Verify H·x = [alpha, 0]
|
||
float dot = v[0] * x[0] + v[1] * x[1];
|
||
float hx0 = x[0] - 2.0f * v[0] * dot;
|
||
float hx1 = x[1] - 2.0f * v[1] * dot;
|
||
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
|
||
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [1, 2, 3] (3D)
|
||
// Reference: norm = √14 ≈ 3.74166
|
||
{
|
||
float x[] = {1.0f, 2.0f, 3.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(14.0f), 1e-6f));
|
||
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(14.0f), 1e-6f));
|
||
|
||
// Verify v is normalized
|
||
float vNorm = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||
REQUIRE_THAT(vNorm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
|
||
// Verify H·x = [alpha, 0, 0]
|
||
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2];
|
||
for (uint8_t i = 0; i < 3; i++) {
|
||
float hx_i = x[i] - 2.0f * v[i] * dot;
|
||
if (i == 0) {
|
||
REQUIRE_THAT(hx_i, Catch::Matchers::WithinRel(alpha, 1e-6f));
|
||
} else {
|
||
REQUIRE_THAT(hx_i, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Test case: [0, 0, 1] (already has leading zeros)
|
||
{
|
||
float x[] = {0.0f, 0.0f, 1.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-1.0f, 1e-6f));
|
||
|
||
// Verify H·x = [-1, 0, 0]
|
||
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2];
|
||
float hx0 = x[0] - 2.0f * v[0] * dot;
|
||
float hx1 = x[1] - 2.0f * v[1] * dot;
|
||
float hx2 = x[2] - 2.0f * v[2] * dot;
|
||
REQUIRE_THAT(hx0, Catch::Matchers::WithinRel(alpha, 1e-6f));
|
||
REQUIRE_THAT(hx1, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
REQUIRE_THAT(hx2, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [5, -3, 2, 1] (4D)
|
||
{
|
||
float x[] = {5.0f, -3.0f, 2.0f, 1.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 4, v, alpha);
|
||
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinRel(sqrtf(39.0f), 1e-6f));
|
||
REQUIRE_THAT(alpha, Catch::Matchers::WithinRel(-sqrtf(39.0f), 1e-6f));
|
||
|
||
// Verify H·x = [alpha, 0, 0, 0]
|
||
float dot = v[0] * x[0] + v[1] * x[1] + v[2] * x[2] + v[3] * x[3];
|
||
for (uint8_t i = 0; i < 4; i++) {
|
||
float hx_i = x[i] - 2.0f * v[i] * dot;
|
||
if (i == 0) {
|
||
REQUIRE_THAT(hx_i, Catch::Matchers::WithinRel(alpha, 1e-6f));
|
||
} else {
|
||
REQUIRE_THAT(hx_i, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Test case: zero vector
|
||
{
|
||
float x[] = {0.0f, 0.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
|
||
float norm = SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
REQUIRE_THAT(norm, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
REQUIRE(alpha == 0.0f);
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 2: ApplyHouseholderLeft
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ApplyHouseholderLeft", "[Matrix][SVD]") {
|
||
// Test: Apply Householder to zero out column 0, rows 1:2 of a 3×3 matrix
|
||
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
||
// After applying HH on col 0 (rows 1:2): A[2,0] should be ~0
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Compute Householder for column 0, rows 1:2 → vector [4, 7]
|
||
float x[] = {4.0f, 7.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
// Apply from left
|
||
SVD::ApplyHouseholderLeft(W, v, 1, 2);
|
||
|
||
// A[2,0] should be ~0
|
||
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify orthogonality of the transformation: W = H·W_original
|
||
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Compute H_left explicitly: I - 2*v*vᵀ (on rows 1:2)
|
||
Matrix<5, 5> H_left{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
H_left[i][i] = 1.0f;
|
||
}
|
||
// Apply -2*v*vᵀ to the sub-block
|
||
float vv = v[0] * v[0] + v[1] * v[1];
|
||
for (uint8_t i = 1; i <= 2; i++) {
|
||
for (uint8_t j = 1; j <= 2; j++) {
|
||
H_left[i][j] -= 2.0f * v[i - 1] * v[j - 1] / vv;
|
||
}
|
||
}
|
||
|
||
// Verify: W ≈ H_left · W_orig
|
||
Matrix<5, 5> HLeftW{0};
|
||
H_left.Mult(W_orig, HLeftW);
|
||
float err = frobeniusNorm5(W - HLeftW);
|
||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||
|
||
// Verify H_left is orthogonal
|
||
REQUIRE(isOrthogonal5(H_left));
|
||
}
|
||
|
||
// Test: Apply to a larger block (4 rows)
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 0, 0, 0, 3.0f, 4.0f, 0, 0,
|
||
0, 5.0f, 6.0f, 0, 0, 0, 7.0f, 8.0f, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Householder on [3, 5, 7] (rows 1:3)
|
||
float x[] = {3.0f, 5.0f, 7.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
|
||
SVD::ApplyHouseholderLeft(W, v, 1, 3);
|
||
|
||
// A[2,0] and A[3,0] should be ~0
|
||
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 3: ApplyHouseholderRight
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ApplyHouseholderRight", "[Matrix][SVD]") {
|
||
// Test: Apply Householder to zero out row 0, cols 1:2 of a 3×3 matrix
|
||
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
||
// After applying HH on row 0 (cols 1:2): A[0,2] should be ~0
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Householder for row 0, cols 1:2 → vector [2, 3]
|
||
float x[] = {2.0f, 3.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
// Apply from right
|
||
SVD::ApplyHouseholderRight(W, v, 1, 2);
|
||
|
||
// A[0,2] should be ~0
|
||
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify W ≈ W_orig · H_right
|
||
Matrix<5, 5> W_orig{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 9.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Compute H_right = I - 2*v*vᵀ (on cols 1:2)
|
||
Matrix<5, 5> H_right{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
H_right[i][i] = 1.0f;
|
||
}
|
||
float vv = v[0] * v[0] + v[1] * v[1];
|
||
for (uint8_t i = 1; i <= 2; i++) {
|
||
for (uint8_t j = 1; j <= 2; j++) {
|
||
H_right[i][j] -= 2.0f * v[i - 1] * v[j - 1] / vv;
|
||
}
|
||
}
|
||
|
||
Matrix<5, 5> WOrigH{0};
|
||
W_orig.Mult(H_right, WOrigH);
|
||
float err = frobeniusNorm5(W - WOrigH);
|
||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||
|
||
// Verify H_right is orthogonal
|
||
REQUIRE(isOrthogonal5(H_right));
|
||
}
|
||
|
||
// Test: Apply to wider block (4 cols)
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 4.0f, 0, 5.0f, 6.0f, 7.0f, 8.0f,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Householder on [2, 3, 4] (cols 1:3)
|
||
float x[] = {2.0f, 3.0f, 4.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
|
||
SVD::ApplyHouseholderRight(W, v, 1, 3);
|
||
|
||
// A[0,2] and A[0,3] should be ~0
|
||
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
REQUIRE_THAT(W.Get(0, 3), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 4: ComputeGivens
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ComputeGivens", "[Matrix][SVD]") {
|
||
// Test case: [3, 4] → c = 0.6, s = 0.8 (3-4-5 triangle)
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 4.0f, c, s);
|
||
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(0.6f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(0.8f, 1e-6f));
|
||
|
||
// Verify: [c s; -s c] · [3; 4] = [5; 0]
|
||
float r = c * 3.0f + s * 4.0f;
|
||
float z = -s * 3.0f + c * 4.0f;
|
||
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify c² + s² = 1
|
||
REQUIRE_THAT(c * c + s * s, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [1, 0] → c = 1, s = 0
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(1.0f, 0.0f, c, s);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [0, 5] → c = 0, s = 1
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(0.0f, 5.0f, c, s);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
|
||
// Verify: [c s; -s c] · [0; 5] = [5; 0]
|
||
float r = c * 0.0f + s * 5.0f;
|
||
float z = -s * 0.0f + c * 5.0f;
|
||
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [-3, -4] → c = -0.6, s = -0.8
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(-3.0f, -4.0f, c, s);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(-0.6f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(-0.8f, 1e-6f));
|
||
|
||
// Verify: [c s; -s c] · [-3; -4] = [5; 0]
|
||
float r = c * (-3.0f) + s * (-4.0f);
|
||
float z = -s * (-3.0f) + c * (-4.0f);
|
||
REQUIRE_THAT(r, Catch::Matchers::WithinRel(5.0f, 1e-6f));
|
||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [1, -1] → c = 1/√2, s = -1/√2 (45°)
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(1.0f, -1.0f, c, s);
|
||
float invSqrt2 = 1.0f / sqrtf(2.0f);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(invSqrt2, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(-invSqrt2, 1e-6f));
|
||
|
||
// Verify: [c s; -s c] · [1; -1] = [√2; 0]
|
||
float r = c * 1.0f + s * (-1.0f);
|
||
float z = -s * 1.0f + c * (-1.0f);
|
||
REQUIRE_THAT(r, Catch::Matchers::WithinRel(sqrtf(2.0f), 1e-6f));
|
||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [0, 0] → c = 1, s = 0 (identity)
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(0.0f, 0.0f, c, s);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
|
||
// Test case: [7, 24] → c = 7/25, s = 24/25 (7-24-25 triangle)
|
||
{
|
||
float c, s;
|
||
SVD::ComputeGivens(7.0f, 24.0f, c, s);
|
||
REQUIRE_THAT(c, Catch::Matchers::WithinRel(7.0f / 25.0f, 1e-6f));
|
||
REQUIRE_THAT(s, Catch::Matchers::WithinRel(24.0f / 25.0f, 1e-6f));
|
||
|
||
float r = c * 7.0f + s * 24.0f;
|
||
float z = -s * 7.0f + c * 24.0f;
|
||
REQUIRE_THAT(r, Catch::Matchers::WithinRel(25.0f, 1e-6f));
|
||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 5: ApplyGivensLeft
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ApplyGivensLeft", "[Matrix][SVD]") {
|
||
// Test: Apply Givens to zero out W[1,0] of a 2×2 matrix
|
||
// Input: [[3, 4], [1, 2]]
|
||
// Givens on rows 0,1 with x=W[0,0]=3, y=W[1,0]=1
|
||
{
|
||
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 1.0f, c, s);
|
||
|
||
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
|
||
|
||
// W[1,0] should be ~0
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify W ≈ G · W_orig
|
||
Matrix<5, 5> W_orig{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Givens rotation matrix (5×5)
|
||
Matrix<5, 5> G{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
G[i][i] = 1.0f;
|
||
}
|
||
G[0][0] = c;
|
||
G[0][1] = s;
|
||
G[1][0] = -s;
|
||
G[1][1] = c;
|
||
|
||
Matrix<5, 5> GW{0};
|
||
G.Mult(W_orig, GW);
|
||
float err = frobeniusNorm5(W - GW);
|
||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify G is orthogonal
|
||
REQUIRE(isOrthogonal5(G));
|
||
}
|
||
|
||
// Test: Apply to larger range of columns
|
||
{
|
||
Matrix<5, 5> W{3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 1.0f, 2.0f, 3.0f, 4.0f,
|
||
5.0f, 0, 0, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 1.0f, c, s);
|
||
|
||
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
|
||
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 6: ApplyGivensRight
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: ApplyGivensRight", "[Matrix][SVD]") {
|
||
// Test: Apply Givens to zero out W[0,1] of a 2×2 matrix
|
||
// Input: [[3, 4], [1, 2]]
|
||
// Givens on cols 0,1 with x=W[0,0]=3, y=W[0,1]=4
|
||
{
|
||
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 4.0f, c, s);
|
||
|
||
SVD::ApplyGivensRight(W, 0, 1, c, s, 0, 4);
|
||
|
||
// W[0,1] should be ~0
|
||
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify W ≈ W_orig · G
|
||
Matrix<5, 5> W_orig{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Givens rotation matrix (5×5)
|
||
Matrix<5, 5> G{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
G[i][i] = 1.0f;
|
||
}
|
||
G[0][0] = c;
|
||
G[0][1] = -s;
|
||
G[1][0] = s;
|
||
G[1][1] = c;
|
||
|
||
Matrix<5, 5> WG{0};
|
||
W_orig.Mult(G, WG);
|
||
float err = frobeniusNorm5(W - WG);
|
||
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
|
||
// Verify G is orthogonal
|
||
REQUIRE(isOrthogonal5(G));
|
||
}
|
||
|
||
// Test: Apply to larger range of rows
|
||
{
|
||
Matrix<5, 5> W{3.0f, 4.0f, 0, 0, 0, 1.0f, 2.0f, 0, 0, 0, 5.0f, 6.0f, 0,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 4.0f, c, s);
|
||
|
||
SVD::ApplyGivensRight(W, 0, 1, c, s, 0, 2);
|
||
|
||
REQUIRE_THAT(W.Get(0, 1), Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 7: Full Bidiagonalization (composing Householder steps)
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: Householder Bidiagonalization",
|
||
"[Matrix][SVD]") {
|
||
// Test: Bidiagonalize a 3×3 matrix and verify reconstruction
|
||
// Input: [[1, 2, 3], [4, 5, 6], [7, 8, 10]]
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 10.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Step 1: Left HH on column 0, rows 1:2 → zero out W[2,0]
|
||
{
|
||
float x[] = {4.0f, 7.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
SVD::ApplyHouseholderLeft(W, v, 1, 2);
|
||
}
|
||
|
||
// Step 2: Right HH on row 0, cols 1:2 → zero out W[0,2]
|
||
{
|
||
float x[] = {W.Get(0, 1), W.Get(0, 2)};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
SVD::ApplyHouseholderRight(W, v, 1, 2);
|
||
}
|
||
|
||
// Step 3: Left HH on column 1, rows 2:2 → nothing to do (single element)
|
||
|
||
// Verify bidiagonal structure: for 3x3, zero elements are A[2][0] (below
|
||
// subdiag in col 0) and A[0][2] (above superdiag in row 0) A[2][1] is the
|
||
// subdiagonal element of col 1 — valid in bidiagonal form
|
||
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||
}
|
||
|
||
// Test: Bidiagonalize a 4×3 matrix
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 3.0f, 0, 0, 4.0f, 5.0f, 6.0f, 0,
|
||
0, 7.0f, 8.0f, 9.0f, 0, 0, 10.0f, 11.0f, 12.0f,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Step 1: Left HH on col 0, rows 1:3 → zero out W[2,0], W[3,0]
|
||
{
|
||
float x[] = {4.0f, 7.0f, 10.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
SVD::ApplyHouseholderLeft(W, v, 1, 3);
|
||
}
|
||
|
||
// Step 2: Right HH on row 0, cols 1:2 → zero out W[0,2]
|
||
{
|
||
float x[] = {W.Get(0, 1), W.Get(0, 2)};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
SVD::ApplyHouseholderRight(W, v, 1, 2);
|
||
}
|
||
|
||
// Step 3: Left HH on col 1, rows 2:3 → zero out W[3,1]
|
||
{
|
||
float x[] = {W.Get(2, 1), W.Get(3, 1)};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
SVD::ApplyHouseholderLeft(W, v, 2, 3);
|
||
}
|
||
|
||
// Verify bidiagonal structure
|
||
REQUIRE_THAT(W.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
REQUIRE_THAT(W.Get(3, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
REQUIRE_THAT(W.Get(3, 1), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
}
|
||
|
||
// Test: Diagonal matrix (no transformations needed)
|
||
{
|
||
Matrix<5, 5> W{10.0f, 0, 0, 0, 0, 0, 5.0f, 0, 0, 0, 0, 0, 2.0f,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Householder on zero vector should be identity
|
||
float x[] = {0.0f, 0.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
// Applying identity should not change anything
|
||
Matrix<5, 5> W_copy{10.0f, 0, 0, 0, 0, 0, 5.0f, 0, 0, 0, 0, 0, 2.0f,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
SVD::ApplyHouseholderLeft(W_copy, v, 1, 2);
|
||
|
||
REQUIRE_THAT(frobeniusNorm5(W - W_copy),
|
||
Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// //
|
||
// ============================================================================
|
||
// TEST 8: Givens QR step on bidiagonal matrix
|
||
// ===========================================================================
|
||
TEST_CASE("SVD Building Block: Givens QR Step on Bidiagonal", "[Matrix][SVD]") {
|
||
// Test: Apply left Givens to zero subdiagonal of a bidiagonal matrix,
|
||
// then apply right Givens with restricted row range to restore bidiagonal
|
||
// form.
|
||
//
|
||
// Input: 3x3 bidiagonal [[1, 2, 0], [3, -4, 5], [0, 6, -7]]
|
||
// Step 1: Left Givens on rows 0,1 with x=W[0][0]=1, y=W[1][0]=3 -> zero
|
||
// W[1][0] Step 2: Right Givens on cols 1,2 with x=W[0][1], y=W[0][2] -> zero
|
||
// W[0][2]
|
||
// Only applied to row 0 (to not reintroduce subdiagonal non-zeros)
|
||
{
|
||
Matrix<5, 5> W{1.0f, 2.0f, 0.0f, 0, 0, 3.0f, -4.0f, 5.0f, 0,
|
||
0, 0.0f, 6.0f, -7.0f, 0, 0, 0, 0, 0,
|
||
0, 0, 0, 0, 0, 0, 0};
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(W.Get(0, 0), W.Get(1, 0), c, s);
|
||
|
||
// Apply from left to zero subdiagonal at W[1][0]
|
||
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
|
||
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
|
||
// After left Givens, W[0][2] may have become non-zero (fill-in from row 0)
|
||
// Apply right Givens to cols 1,2 with x=W[0][1], y=W[0][2] -> zero W[0][2]
|
||
// Only apply to rows 0 (to preserve bidiagonal structure below row 0)
|
||
float c2, s2;
|
||
SVD::ComputeGivens(W.Get(0, 1), W.Get(0, 2), c2, s2);
|
||
SVD::ApplyGivensRight(W, 1, 2, c2, s2, 0, 0);
|
||
|
||
// Should be bidiagonal: W[1][0] ~ 0 (from left Givens), W[0][2] ~ 0 (from
|
||
// right Givens)
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
}
|
||
|
||
// Test: Verify that a full QR step (left + right Givens) preserves the
|
||
// bidiagonal structure when applied correctly with proper row ranges.
|
||
{
|
||
Matrix<5, 5> W{2.0f, 3.0f, 0, 0, 0, -1.0f, 4.0f, 5.0f, 0, 0, 0, 6.0f, -7.0f,
|
||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||
|
||
// Left Givens on col 0 (rows 0,1)
|
||
float c, s;
|
||
SVD::ComputeGivens(W.Get(0, 0), W.Get(1, 0), c, s);
|
||
SVD::ApplyGivensLeft(W, 0, 1, c, s, 0, 4);
|
||
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
|
||
// Right Givens on row 0 (cols 1,2) - only affect row 0
|
||
float c2, s2;
|
||
SVD::ComputeGivens(W.Get(0, 1), W.Get(0, 2), c2, s2);
|
||
SVD::ApplyGivensRight(W, 1, 2, c2, s2, 0, 0);
|
||
|
||
// Bidiagonal structure preserved
|
||
REQUIRE_THAT(W.Get(1, 0), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
REQUIRE_THAT(W.Get(0, 2), Catch::Matchers::WithinAbs(0.0f, 1e-5f));
|
||
}
|
||
}
|
||
|
||
// ============================================================================TEST
|
||
// 9: Orthogonality preservation of Householder transformations
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: Householder preserves orthogonality",
|
||
"[Matrix][SVD]") {
|
||
// Starting with an orthogonal matrix, applying Householder should preserve it
|
||
{
|
||
// Identity matrix is orthogonal
|
||
Matrix<5, 5> M{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
M[i][i] = 1.0f;
|
||
}
|
||
|
||
// Householder on first 3 elements of column 0
|
||
float x[] = {1.0f, 0.0f, 0.0f};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 3, v, alpha);
|
||
|
||
// Apply from left
|
||
Matrix<5, 5> M_left = M;
|
||
SVD::ApplyHouseholderLeft(M_left, v, 0, 2);
|
||
|
||
// M_left should still be orthogonal
|
||
REQUIRE(isOrthogonal5(M_left));
|
||
|
||
// Apply from right
|
||
Matrix<5, 5> M_right = M;
|
||
SVD::ApplyHouseholderRight(M_right, v, 0, 2);
|
||
|
||
REQUIRE(isOrthogonal5(M_right));
|
||
}
|
||
|
||
// Random orthogonal matrix (rotation)
|
||
{
|
||
float c = sqrtf(0.5f);
|
||
float s = sqrtf(0.5f);
|
||
Matrix<5, 5> M{0};
|
||
M[0][0] = c;
|
||
M[0][1] = -s;
|
||
M[1][0] = s;
|
||
M[1][1] = c;
|
||
for (uint8_t i = 2; i < 5; i++) {
|
||
M[i][i] = 1.0f;
|
||
}
|
||
|
||
REQUIRE(isOrthogonal5(M));
|
||
|
||
// Apply Householder on rows 0,1
|
||
float x[] = {c, s};
|
||
float v[5] = {0};
|
||
float alpha = 0;
|
||
SVD::ComputeHouseholder(x, 2, v, alpha);
|
||
|
||
Matrix<5, 5> M_test = M;
|
||
SVD::ApplyHouseholderLeft(M_test, v, 0, 1);
|
||
|
||
REQUIRE(isOrthogonal5(M_test));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// TEST 10: Orthogonality preservation of Givens transformations
|
||
// ============================================================================
|
||
TEST_CASE("SVD Building Block: Givens preserves orthogonality",
|
||
"[Matrix][SVD]") {
|
||
// Starting with an orthogonal matrix, applying Givens should preserve it
|
||
{
|
||
Matrix<5, 5> M{0};
|
||
for (uint8_t i = 0; i < 5; i++) {
|
||
M[i][i] = 1.0f;
|
||
}
|
||
|
||
float c, s;
|
||
SVD::ComputeGivens(3.0f, 4.0f, c, s);
|
||
|
||
// Apply from left
|
||
Matrix<5, 5> M_left = M;
|
||
SVD::ApplyGivensLeft(M_left, 0, 1, c, s, 0, 4);
|
||
|
||
REQUIRE(isOrthogonal5(M_left));
|
||
|
||
// Apply from right
|
||
Matrix<5, 5> M_right = M;
|
||
SVD::ApplyGivensRight(M_right, 0, 1, c, s, 0, 4);
|
||
|
||
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));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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};
|
||
// JacobiEigenSymmetric operates on Matrix<N,N> — copy the raw test
|
||
// data in, run the solver, copy the eigenvector matrix back out.
|
||
Matrix<5, 5> Tm{0};
|
||
for (int i = 0; i < 5; i++)
|
||
for (int j = 0; j < 5; j++)
|
||
Tm[i][j] = T[i][j];
|
||
Matrix<5, 5> Vm{0};
|
||
SVD::JacobiEigenSymmetric(Tm, ns[c], evals, Vm);
|
||
float V[5][5] = {{0}};
|
||
for (int i = 0; i < 5; i++)
|
||
for (int j = 0; j < 5; j++)
|
||
V[i][j] = Vm[i][j];
|
||
|
||
// 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));
|
||
}
|
||
}
|