Files
Vector3D/unit-tests/svd-build-blocks-tests.cpp
T
2026-08-26 13:08:21 -04:00

1648 lines
56 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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));
}
}