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

1550 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 "QR.hpp"
#include "SVD.hpp"
// any other libraries
#include <array>
#include <cmath>
#include <iostream>
// Helper functions
template <uint8_t rows, uint8_t columns>
float matrixSum(const Matrix<rows, columns> &matrix) {
float sum = 0;
for (uint32_t i = 0; i < rows * columns; i++) {
float number = matrix.ToArray()[i];
sum += number * number;
}
return std::sqrt(sum);
}
template <uint8_t rows, uint8_t columns>
void printLabeledMatrix(const std::string &label,
const Matrix<rows, columns> &matrix) {
std::string strBuf = "";
matrix.ToString(strBuf);
std::cout << label << ":\n" << strBuf << std::endl;
}
TEST_CASE("Initialization", "Matrix") {
SECTION("Array Initialization") {
std::array<float, 4> arr2{5, 6, 7, 8};
Matrix<2, 2> mat2{arr2};
// array initialization
REQUIRE(mat2.Get(0, 0) == 5);
REQUIRE(mat2.Get(0, 1) == 6);
REQUIRE(mat2.Get(1, 0) == 7);
REQUIRE(mat2.Get(1, 1) == 8);
}
SECTION("Argument Pack Initialization") {
Matrix<2, 2> mat1{1, 2, 3, 4};
// template pack initialization
REQUIRE(mat1.Get(0, 0) == 1);
REQUIRE(mat1.Get(0, 1) == 2);
REQUIRE(mat1.Get(1, 0) == 3);
REQUIRE(mat1.Get(1, 1) == 4);
}
SECTION("Single Argument Pack Initialization") {
Matrix<2, 2> mat1{2};
// template pack initialization
REQUIRE(mat1.Get(0, 0) == 2);
REQUIRE(mat1.Get(0, 1) == 2);
REQUIRE(mat1.Get(1, 0) == 2);
REQUIRE(mat1.Get(1, 1) == 2);
}
}
TEST_CASE("Elementary Matrix Operations", "Matrix") {
std::array<float, 4> arr2{5, 6, 7, 8};
Matrix<2, 2> mat1{1, 2, 3, 4};
Matrix<2, 2> mat2{arr2};
Matrix<2, 2> mat3{};
SECTION("Fill") {
mat1.Fill(0);
REQUIRE(mat1.Get(0, 0) == 0);
REQUIRE(mat1.Get(0, 1) == 0);
REQUIRE(mat1.Get(1, 0) == 0);
REQUIRE(mat1.Get(1, 1) == 0);
mat2.Fill(100000);
REQUIRE(mat2.Get(0, 0) == 100000);
REQUIRE(mat2.Get(0, 1) == 100000);
REQUIRE(mat2.Get(1, 0) == 100000);
REQUIRE(mat2.Get(1, 1) == 100000);
mat3.Fill(-20);
REQUIRE(mat3.Get(0, 0) == -20);
REQUIRE(mat3.Get(0, 1) == -20);
REQUIRE(mat3.Get(1, 0) == -20);
REQUIRE(mat3.Get(1, 1) == -20);
}
SECTION("Addition") {
mat1.Add(mat2, mat3);
REQUIRE(mat3.Get(0, 0) == 6);
REQUIRE(mat3.Get(0, 1) == 8);
REQUIRE(mat3.Get(1, 0) == 10);
REQUIRE(mat3.Get(1, 1) == 12);
// try out addition with overloaded operators
mat3.Fill(0);
mat3 = mat1 + mat2;
REQUIRE(mat3.Get(0, 0) == 6);
REQUIRE(mat3.Get(0, 1) == 8);
REQUIRE(mat3.Get(1, 0) == 10);
REQUIRE(mat3.Get(1, 1) == 12);
}
SECTION("Subtraction") {
mat1.Sub(mat2, mat3);
REQUIRE(mat3.Get(0, 0) == -4);
REQUIRE(mat3.Get(0, 1) == -4);
REQUIRE(mat3.Get(1, 0) == -4);
REQUIRE(mat3.Get(1, 1) == -4);
// try out subtraction with operators
mat3.Fill(0);
mat3 = mat1 - mat2;
REQUIRE(mat3.Get(0, 0) == -4);
REQUIRE(mat3.Get(0, 1) == -4);
REQUIRE(mat3.Get(1, 0) == -4);
REQUIRE(mat3.Get(1, 1) == -4);
}
SECTION("Multiplication") {
mat1.Mult(mat2, mat3);
REQUIRE(mat3.Get(0, 0) == 19);
REQUIRE(mat3.Get(0, 1) == 22);
REQUIRE(mat3.Get(1, 0) == 43);
REQUIRE(mat3.Get(1, 1) == 50);
// try out multiplication with operators
mat3.Fill(0);
mat3 = mat1 * mat2;
REQUIRE(mat3.Get(0, 0) == 19);
REQUIRE(mat3.Get(0, 1) == 22);
REQUIRE(mat3.Get(1, 0) == 43);
REQUIRE(mat3.Get(1, 1) == 50);
// Non-square multiplication
Matrix<2, 4> mat4{1, 2, 3, 4, 5, 6, 7, 8};
Matrix<4, 2> mat5{9, 10, 11, 12, 13, 14, 15, 16};
Matrix<2, 2> mat6{};
mat6 = mat4 * mat5;
REQUIRE(mat6.Get(0, 0) == 130);
REQUIRE(mat6.Get(0, 1) == 140);
REQUIRE(mat6.Get(1, 0) == 322);
REQUIRE(mat6.Get(1, 1) == 348);
// One more non-square multiplicaiton
Matrix<4, 4> mat7{};
mat7 = mat5 * mat4;
REQUIRE(mat7.Get(0, 0) == 59);
REQUIRE(mat7.Get(0, 1) == 78);
REQUIRE(mat7.Get(0, 2) == 97);
REQUIRE(mat7.Get(0, 3) == 116);
REQUIRE(mat7.Get(1, 0) == 71);
REQUIRE(mat7.Get(1, 1) == 94);
REQUIRE(mat7.Get(1, 2) == 117);
REQUIRE(mat7.Get(1, 3) == 140);
REQUIRE(mat7.Get(2, 0) == 83);
REQUIRE(mat7.Get(2, 1) == 110);
REQUIRE(mat7.Get(2, 2) == 137);
REQUIRE(mat7.Get(2, 3) == 164);
REQUIRE(mat7.Get(3, 0) == 95);
REQUIRE(mat7.Get(3, 1) == 126);
REQUIRE(mat7.Get(3, 2) == 157);
REQUIRE(mat7.Get(3, 3) == 188);
}
SECTION("Scalar Multiplication") {
mat1.Mult(2, mat3);
REQUIRE(mat3.Get(0, 0) == 2);
REQUIRE(mat3.Get(0, 1) == 4);
REQUIRE(mat3.Get(1, 0) == 6);
REQUIRE(mat3.Get(1, 1) == 8);
}
SECTION("Element Multiply") {
mat1.ElementMultiply(mat2, mat3);
REQUIRE(mat3.Get(0, 0) == 5);
REQUIRE(mat3.Get(0, 1) == 12);
REQUIRE(mat3.Get(1, 0) == 21);
REQUIRE(mat3.Get(1, 1) == 32);
}
SECTION("Element Divide") {
mat1.ElementDivide(mat2, mat3);
REQUIRE_THAT(mat3.Get(0, 0), Catch::Matchers::WithinRel(0.2f, 1e-6f));
REQUIRE_THAT(mat3.Get(0, 1), Catch::Matchers::WithinRel(0.3333333f, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 0), Catch::Matchers::WithinRel(0.4285714f, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 1), Catch::Matchers::WithinRel(0.5f, 1e-6f));
}
SECTION("Minor Matrix") {
// what about matrices of 0,0 or 1,1?
// minor matrix for 2x2 matrix
Matrix<1, 1> minorMat1{};
mat1.MinorMatrix(minorMat1, 0, 0);
REQUIRE(minorMat1.Get(0, 0) == 4);
mat1.MinorMatrix(minorMat1, 0, 1);
REQUIRE(minorMat1.Get(0, 0) == 3);
mat1.MinorMatrix(minorMat1, 1, 0);
REQUIRE(minorMat1.Get(0, 0) == 2);
mat1.MinorMatrix(minorMat1, 1, 1);
REQUIRE(minorMat1.Get(0, 0) == 1);
// minor matrix for 3x3 matrix
Matrix<3, 3> mat4{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<2, 2> minorMat4{};
mat4.MinorMatrix(minorMat4, 0, 0);
REQUIRE(minorMat4.Get(0, 0) == 5);
REQUIRE(minorMat4.Get(0, 1) == 6);
REQUIRE(minorMat4.Get(1, 0) == 8);
REQUIRE(minorMat4.Get(1, 1) == 9);
mat4.MinorMatrix(minorMat4, 1, 1);
REQUIRE(minorMat4.Get(0, 0) == 1);
REQUIRE(minorMat4.Get(0, 1) == 3);
REQUIRE(minorMat4.Get(1, 0) == 7);
REQUIRE(minorMat4.Get(1, 1) == 9);
mat4.MinorMatrix(minorMat4, 2, 2);
REQUIRE(minorMat4.Get(0, 0) == 1);
REQUIRE(minorMat4.Get(0, 1) == 2);
REQUIRE(minorMat4.Get(1, 0) == 4);
REQUIRE(minorMat4.Get(1, 1) == 5);
}
SECTION("Determinant") {
float det1 = mat1.Det();
REQUIRE_THAT(det1, Catch::Matchers::WithinRel(-2.0F, 1e-6f));
Matrix<3, 3> mat4{1, 2, 3, 4, 5, 6, 7, 8, 9};
float det4 = mat4.Det();
REQUIRE_THAT(det4, Catch::Matchers::WithinRel(0.0F, 1e-6f));
Matrix<3, 3> mat5{1, 0, 0, 0, 2, 0, 0, 0, 3};
float det5 = mat5.Det();
REQUIRE_THAT(det5, Catch::Matchers::WithinRel(6.0F, 1e-6f));
}
SECTION("Matrix of Minors") {
mat1.MatrixOfMinors(mat3);
REQUIRE_THAT(mat3.Get(0, 0), Catch::Matchers::WithinRel(4.0F, 1e-6f));
REQUIRE_THAT(mat3.Get(0, 1), Catch::Matchers::WithinRel(3.0F, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 0), Catch::Matchers::WithinRel(2.0F, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 1), Catch::Matchers::WithinRel(1.0F, 1e-6f));
Matrix<3, 3> mat4{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<3, 3> mat5{0};
mat4.MatrixOfMinors(mat5);
REQUIRE_THAT(mat5.Get(0, 0), Catch::Matchers::WithinRel(-3.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(0, 1), Catch::Matchers::WithinRel(-6.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(0, 2), Catch::Matchers::WithinRel(-3.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(1, 0), Catch::Matchers::WithinRel(-6.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(1, 1), Catch::Matchers::WithinRel(-12.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(1, 2), Catch::Matchers::WithinRel(-6.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(2, 0), Catch::Matchers::WithinRel(-3.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(2, 1), Catch::Matchers::WithinRel(-6.0F, 1e-6f));
REQUIRE_THAT(mat5.Get(2, 2), Catch::Matchers::WithinRel(-3.0F, 1e-6f));
}
SECTION("Invert") {
mat3 = mat1.Invert();
REQUIRE_THAT(mat3.Get(0, 0), Catch::Matchers::WithinRel(-2.0F, 1e-6f));
REQUIRE_THAT(mat3.Get(0, 1), Catch::Matchers::WithinRel(1.0F, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 0), Catch::Matchers::WithinRel(1.5F, 1e-6f));
REQUIRE_THAT(mat3.Get(1, 1), Catch::Matchers::WithinRel(-0.5F, 1e-6f));
};
SECTION("Transpose") {
// transpose a square matrix
mat3 = mat1.Transpose();
REQUIRE(mat3.Get(0, 0) == 1);
REQUIRE(mat3.Get(0, 1) == 3);
REQUIRE(mat3.Get(1, 0) == 2);
REQUIRE(mat3.Get(1, 1) == 4);
// transpose a non-square matrix
Matrix<2, 3> mat4{1, 2, 3, 4, 5, 6};
Matrix<3, 2> mat5{};
mat5 = mat4.Transpose();
REQUIRE(mat5.Get(0, 0) == 1);
REQUIRE(mat5.Get(0, 1) == 4);
REQUIRE(mat5.Get(1, 0) == 2);
REQUIRE(mat5.Get(1, 1) == 5);
REQUIRE(mat5.Get(2, 0) == 3);
REQUIRE(mat5.Get(2, 1) == 6);
}
SECTION("GET ROW") {
Matrix<1, 2> mat1Rows{};
mat1.GetRow(0, mat1Rows);
REQUIRE(mat1Rows.Get(0, 0) == 1);
REQUIRE(mat1Rows.Get(0, 1) == 2);
mat1.GetRow(1, mat1Rows);
REQUIRE(mat1Rows.Get(0, 0) == 3);
REQUIRE(mat1Rows.Get(0, 1) == 4);
}
SECTION("GET COLUMN") {
Matrix<2, 1> mat1Columns{};
mat1.GetColumn(0, mat1Columns);
REQUIRE(mat1Columns.Get(0, 0) == 1);
REQUIRE(mat1Columns.Get(1, 0) == 3);
mat1.GetColumn(1, mat1Columns);
REQUIRE(mat1Columns.Get(0, 0) == 2);
REQUIRE(mat1Columns.Get(1, 0) == 4);
}
SECTION("Get Sub-Matrices") {
Matrix<3, 3> mat4{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<2, 2> mat5 = mat4.SubMatrix<2, 2, 0, 0>();
REQUIRE(mat5.Get(0, 0) == 1);
REQUIRE(mat5.Get(0, 1) == 2);
REQUIRE(mat5.Get(1, 0) == 4);
REQUIRE(mat5.Get(1, 1) == 5);
mat5 = mat4.SubMatrix<2, 2, 1, 1>();
REQUIRE(mat5.Get(0, 0) == 5);
REQUIRE(mat5.Get(0, 1) == 6);
REQUIRE(mat5.Get(1, 0) == 8);
REQUIRE(mat5.Get(1, 1) == 9);
Matrix<3, 1> mat6 = mat4.SubMatrix<3, 1, 0, 0>();
REQUIRE(mat6.Get(0, 0) == 1);
REQUIRE(mat6.Get(1, 0) == 4);
REQUIRE(mat6.Get(2, 0) == 7);
Matrix<1, 3> mat7 = mat4.SubMatrix<1, 3, 0, 0>();
REQUIRE(mat7.Get(0, 0) == 1);
REQUIRE(mat7.Get(0, 1) == 2);
REQUIRE(mat7.Get(0, 2) == 3);
}
SECTION("Set Sub-Matrices") {
Matrix<3, 3> startMatrix{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<3, 3> mat4 = startMatrix;
Matrix<2, 2> mat5{10, 11, 12, 13};
mat4.SetSubMatrix(0, 0, mat5);
REQUIRE(mat4.Get(0, 0) == 10);
REQUIRE(mat4.Get(0, 1) == 11);
REQUIRE(mat4.Get(1, 0) == 12);
REQUIRE(mat4.Get(1, 1) == 13);
mat4 = startMatrix;
mat4.SetSubMatrix(1, 1, mat5);
REQUIRE(mat4.Get(1, 1) == 10);
REQUIRE(mat4.Get(1, 2) == 11);
REQUIRE(mat4.Get(2, 1) == 12);
REQUIRE(mat4.Get(2, 2) == 13);
Matrix<3, 1> mat6{10, 11, 12};
mat4.SetSubMatrix(0, 0, mat6);
REQUIRE(mat4.Get(0, 0) == 10);
REQUIRE(mat4.Get(1, 0) == 11);
REQUIRE(mat4.Get(2, 0) == 12);
Matrix<1, 3> mat7{10, 11, 12};
mat4.SetSubMatrix(0, 0, mat7);
REQUIRE(mat4.Get(0, 0) == 10);
REQUIRE(mat4.Get(0, 1) == 11);
REQUIRE(mat4.Get(0, 2) == 12);
}
}
TEST_CASE("Identity Matrix", "Matrix") {
SECTION("Square Matrix") {
Matrix<5, 5> matrix = Matrix<5, 5>::Identity();
uint32_t oneColumnIndex{0};
for (uint32_t row = 0; row < 5; row++) {
for (uint32_t column = 0; column < 5; column++) {
float value = matrix[row][column];
if (oneColumnIndex == column) {
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
} else {
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
oneColumnIndex++;
}
}
SECTION("Wide Matrix") {
Matrix<2, 5> matrix = Matrix<2, 5>::Identity();
uint32_t oneColumnIndex{0};
for (uint32_t row = 0; row < 2; row++) {
for (uint32_t column = 0; column < 5; column++) {
float value = matrix[row][column];
if (oneColumnIndex == column && row < 3) {
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
} else {
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
oneColumnIndex++;
}
}
SECTION("Tall Matrix") {
Matrix<5, 2> matrix = Matrix<5, 2>::Identity();
uint32_t oneColumnIndex{0};
for (uint32_t row = 0; row < 5; row++) {
for (uint32_t column = 0; column < 2; column++) {
float value = matrix[row][column];
if (oneColumnIndex == column) {
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
} else {
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
}
oneColumnIndex++;
}
}
}
// TODO: Add test for scalar division
TEST_CASE("Euclidean Norm", "Matrix") {
SECTION("2x2 Normalize") {
Matrix<2, 2> mat1{1, 2, 3, 4};
Matrix<2, 2> mat2{};
mat2 = mat1 / mat1.EuclideanNorm();
float sqrt_30{static_cast<float>(sqrt(30.0f))};
REQUIRE(mat2.Get(0, 0) == 1 / sqrt_30);
REQUIRE(mat2.Get(0, 1) == 2 / sqrt_30);
REQUIRE(mat2.Get(1, 0) == 3 / sqrt_30);
REQUIRE(mat2.Get(1, 1) == 4 / sqrt_30);
REQUIRE_THAT(matrixSum(mat2), Catch::Matchers::WithinRel(1.0f, 1e-6f));
}
SECTION("2x1 (Vector) Normalize") {
Matrix<2, 1> mat1{-0.878877044, 2.92092276};
Matrix<2, 1> mat2{};
mat2 = mat1 / mat1.EuclideanNorm();
REQUIRE_THAT(mat2.Get(0, 0),
Catch::Matchers::WithinRel(-0.288129855179f, 1e-6f));
REQUIRE_THAT(mat2.Get(1, 0),
Catch::Matchers::WithinRel(0.957591346325f, 1e-6f));
float sum = matrixSum(mat2);
REQUIRE_THAT(sum, Catch::Matchers::WithinRel(1.0f, 1e-6f));
}
SECTION("Normalized vectors sum to 1") {
Matrix<9, 1> mat1{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<9, 1> mat2;
mat2 = mat1 / mat1.EuclideanNorm();
float sum = matrixSum(mat2);
REQUIRE_THAT(sum, Catch::Matchers::WithinRel(1.0f, 1e-6f));
Matrix<2, 3> mat3{1, 2, 3, 4, 5, 6};
Matrix<2, 3> mat4{};
mat4 = mat3 / mat3.EuclideanNorm();
sum = matrixSum(mat4);
REQUIRE_THAT(sum, Catch::Matchers::WithinRel(1.0f, 1e-6f));
}
}
TEST_CASE("QR Decompositions", "Matrix") {
SECTION("2x2 QRDecomposition") {
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
Matrix<2, 2> Q{}, R{};
A.QRDecomposition(Q, R);
// Check that Q * R ≈ A
Matrix<2, 2> QR{};
Q.Mult(R, QR);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
REQUIRE_THAT(QR[i][j], Catch::Matchers::WithinRel(A[i][j], 1e-4f));
}
}
// Check that Q is orthonormal: Qᵀ * Q ≈ I
Matrix<2, 2> Qt = Q.Transpose();
Matrix<2, 2> QtQ{};
Qt.Mult(Q, QtQ);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
if (i == j)
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinRel(1.0f, 1e-4f));
else
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
}
// Optional: R should be upper triangular
REQUIRE(std::fabs(R[1][0]) < 1e-4f);
// check that all Q values are correct
REQUIRE_THAT(Q[0][0], Catch::Matchers::WithinRel(0.3162f, 1e-4f));
REQUIRE_THAT(Q[0][1], Catch::Matchers::WithinRel(0.94868f, 1e-4f));
REQUIRE_THAT(Q[1][0], Catch::Matchers::WithinRel(0.94868f, 1e-4f));
REQUIRE_THAT(Q[1][1], Catch::Matchers::WithinRel(-0.3162f, 1e-4f));
// check that all R values are correct
REQUIRE_THAT(R[0][0], Catch::Matchers::WithinRel(3.16228f, 1e-4f));
REQUIRE_THAT(R[0][1], Catch::Matchers::WithinRel(4.42719f, 1e-4f));
REQUIRE_THAT(R[1][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(R[1][1], Catch::Matchers::WithinRel(0.63246f, 1e-4f));
}
SECTION("3x3 QRDecomposition") {
// this symmetrix tridiagonal matrix is well behaved for testing
Matrix<3, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
Matrix<3, 3> Q{}, R{};
A.QRDecomposition(Q, R);
// Check that Q * R ≈ A
Matrix<3, 3> QR{};
QR = Q * R;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
REQUIRE_THAT(QR[i][j], Catch::Matchers::WithinRel(A[i][j], 1e-4f));
}
}
// Check that Qᵀ * Q ≈ I
// Since the rank of this matrix is 2, only the top left 2x2 sub-matrix will
// equal I.
Matrix<3, 3> Qt = Q.Transpose();
Matrix<3, 3> QtQ{};
QtQ = Qt * Q;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
if (i == j)
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinRel(1.0f, 1e-4f));
else
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
}
// Optional: Check R is upper triangular
for (int i = 1; i < 3; ++i) {
for (int j = 0; j < i; ++j) {
REQUIRE(std::fabs(R[i][j]) < 1e-4f);
}
}
}
SECTION("4x2 QRDecomposition") {
// A simple 4x2 matrix
Matrix<4, 2> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f};
Matrix<4, 2> Q{};
Matrix<2, 2> R{};
A.QRDecomposition(Q, R);
// Check that Q * R ≈ A
Matrix<4, 2> QR{};
Q.Mult(R, QR);
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 2; ++j) {
REQUIRE_THAT(QR[i][j], Catch::Matchers::WithinRel(A[i][j], 1e-4f));
}
}
// Check that Qᵀ * Q ≈ I₂
Matrix<2, 4> Qt = Q.Transpose();
Matrix<2, 2> QtQ{};
Qt.Mult(Q, QtQ);
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
if (i == j)
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinRel(1.0f, 1e-4f));
else
REQUIRE_THAT(QtQ[i][j], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
}
// Check R is upper triangular (i > j ⇒ R[i][j] ≈ 0)
for (int i = 1; i < 2; ++i) {
for (int j = 0; j < i; ++j) {
REQUIRE(std::fabs(R[i][j]) < 1e-4f);
}
}
}
}
// ============================================================================
// Eigen QR Helpers (scipy references; eigenvector checks are sign-invariant)
// ============================================================================
/**
* @brief Normalized eigenpair residual ||A v - lambda v|| / (||A||_F + |lambda|)
*/
template <uint8_t N>
static float eigenResidual(const Matrix<N, N> &A, float lambda,
const Matrix<N, 1> &v) {
Matrix<N, 1> Av{};
A.Mult(v, Av);
float sum = 0.0f;
float frob = 0.0f;
for (uint8_t i = 0; i < N; i++) {
float d = Av.Get(i, 0) - lambda * v.Get(i, 0);
sum += d * d;
for (uint8_t j = 0; j < N; j++) {
float a = A.Get(i, j);
frob += a * a;
}
}
float scale = sqrtf(frob) + fabsf(lambda);
return sqrtf(sum) / scale;
}
/**
* @brief Column of the eigenvector matrix; used for the residual check.
*/
template <uint8_t N>
static Matrix<N, 1> eigenColumn(const Matrix<N, N> &V, uint8_t col) {
Matrix<N, 1> v{};
for (uint8_t i = 0; i < N; i++) {
v[i][0] = V.Get(i, col);
}
return v;
}
/**
* @brief Check V^T V ~ I (eigenvectors orthonormal).
*/
template <uint8_t N>
static bool isOrthogonal(const Matrix<N, N> &V, float tol = 1e-4f) {
Matrix<N, N> Vt = V.Transpose();
Matrix<N, N> VtV{};
Vt.Mult(V, VtV);
for (uint8_t i = 0; i < N; i++) {
for (uint8_t j = 0; j < N; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
if (fabsf(VtV.Get(i, j) - expected) > tol) {
return false;
}
}
}
return true;
}
/**
* @brief Sign-invariant component check: |actual| within max(1e-4, 1e-3*|ref|)
* of ref (ref is the ABSOLUTE value from the scipy reference).
*/
static bool componentMatches(float actual, float refAbs) {
float a = fabsf(actual);
float tol = 1e-4f;
if (refAbs * 1e-3f > tol) {
tol = refAbs * 1e-3f;
}
return fabsf(a - refAbs) <= tol;
}
TEST_CASE("Eigenvalues and Vectors", "Matrix") {
SECTION("2x2 Eigen (nonsymmetric, closed form)") {
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
Matrix<2, 2> vectors{};
Matrix<2, 1> values{};
A.EigenQR(vectors, values, 1000000, 1e-20f);
REQUIRE_THAT(vectors[0][0], Catch::Matchers::WithinRel(0.41597f, 1e-4f));
REQUIRE_THAT(vectors[1][0], Catch::Matchers::WithinRel(0.90938f, 1e-4f));
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(5.372282f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(-0.372281f, 1e-4f));
}
// Reference values: numpy.linalg.eigh on float32 matrices.
// Eigenvector component references are ABSOLUTE values (signs arbitrary).
SECTION("3x3 Symmetric Eigen") {
Matrix<3, 3> A{1, 2, 3, 2, 5, 8, 3, 8, 9};
Matrix<3, 3> vectors{};
Matrix<3, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(16.102417f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(0.191920f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(-1.2943381f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.231657207f));
REQUIRE(componentMatches(vectors[1][0], 0.59582746f));
REQUIRE(componentMatches(vectors[2][0], 0.768976331f));
REQUIRE(componentMatches(vectors[0][1], 0.956842422f));
REQUIRE(componentMatches(vectors[1][1], 0.282139271f));
REQUIRE(componentMatches(vectors[2][1], 0.0696421042f));
REQUIRE(componentMatches(vectors[0][2], 0.175463736f));
REQUIRE(componentMatches(vectors[1][2], 0.75192225f));
REQUIRE(componentMatches(vectors[2][2], 0.635472536f));
// eigenvectors orthonormal; eigenpair residuals small
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 3; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("3x3 Rank Deficient Eigen") {
// A = v v^T with v = [1, 2, 3]: eigenvalues {14, 0, 0}
Matrix<3, 3> A{1, 2, 3, 2, 4, 6, 3, 6, 9};
Matrix<3, 3> vectors{};
Matrix<3, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(14.0f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// dominant eigenvector is v/|v| (sign-invariant); the two null-space
// eigenvectors may be ANY orthonormal basis of the null plane, so only
// orthogonality + residuals are checked for the full matrix.
REQUIRE(componentMatches(vectors[0][0], 0.267261237f));
REQUIRE(componentMatches(vectors[1][0], 0.534522474f));
REQUIRE(componentMatches(vectors[2][0], 0.801783741f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 3; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("4x4 Symmetric Eigen") {
Matrix<4, 4> A{2, 1, 0, 1, 1, 3, 1, 0, 0, 1, 4, 1, 1, 0, 1, 5};
Matrix<4, 4> vectors{};
Matrix<4, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues are exactly {6, 4, 3, 1}
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(6.0f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(4.0f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(3.0f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(1.0f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.258198887f));
REQUIRE(componentMatches(vectors[1][0], 0.258198887f));
REQUIRE(componentMatches(vectors[2][0], 0.516397774f));
REQUIRE(componentMatches(vectors[3][0], 0.774596691f));
REQUIRE(componentMatches(vectors[0][1], 0.0f));
REQUIRE(componentMatches(vectors[1][1], 0.577350259f));
REQUIRE(componentMatches(vectors[2][1], 0.577350259f));
REQUIRE(componentMatches(vectors[3][1], 0.577350259f));
REQUIRE(componentMatches(vectors[0][2], 0.577350259f));
REQUIRE(componentMatches(vectors[1][2], 0.577350259f));
REQUIRE(componentMatches(vectors[2][2], 0.577350259f));
REQUIRE(componentMatches(vectors[3][2], 0.0f));
REQUIRE(componentMatches(vectors[0][3], 0.774596691f));
REQUIRE(componentMatches(vectors[1][3], 0.516397774f));
REQUIRE(componentMatches(vectors[2][3], 0.258198887f));
REQUIRE(componentMatches(vectors[3][3], 0.258198887f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 4; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("5x5 Symmetric Eigen") {
Matrix<5, 5> A{3, 1, 0, 0, 1, 1, 4, 1, 0, 0, 0, 1, 5, 1, 0, 0, 0, 1, 6, 1,
1, 0, 0, 1, 7};
Matrix<5, 5> vectors{};
Matrix<5, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(7.90154457f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(6.20044184f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(5.14503145f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(3.61823463f, 1e-4f));
REQUIRE_THAT(values[4][0], Catch::Matchers::WithinRel(2.13474774f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.182430908f));
REQUIRE(componentMatches(vectors[1][0], 0.102749094f));
REQUIRE(componentMatches(vectors[2][0], 0.21844925f));
REQUIRE(componentMatches(vectors[3][0], 0.531091094f));
REQUIRE(componentMatches(vectors[4][0], 0.791444063f));
REQUIRE(componentMatches(vectors[0][1], 0.0877681747f));
REQUIRE(componentMatches(vectors[1][1], 0.245861098f));
REQUIRE(componentMatches(vectors[2][1], 0.628771126f));
REQUIRE(componentMatches(vectors[3][1], 0.508941948f));
REQUIRE(componentMatches(vectors[4][1], 0.526758015f));
REQUIRE(componentMatches(vectors[0][2], 0.349721253f));
REQUIRE(componentMatches(vectors[1][2], 0.628706098f));
REQUIRE(componentMatches(vectors[2][2], 0.370167077f));
REQUIRE(componentMatches(vectors[3][2], 0.575020194f));
REQUIRE(componentMatches(vectors[4][2], 0.121457018f));
REQUIRE(componentMatches(vectors[0][3], 0.429638386f));
REQUIRE(componentMatches(vectors[1][3], 0.498553723f));
REQUIRE(componentMatches(vectors[2][3], 0.619968951f));
REQUIRE(componentMatches(vectors[3][3], 0.35809797f));
REQUIRE(componentMatches(vectors[4][3], 0.232936427f));
REQUIRE(componentMatches(vectors[0][4], 0.807540476f));
REQUIRE(componentMatches(vectors[1][4], 0.534011006f));
REQUIRE(componentMatches(vectors[2][4], 0.188524753f));
REQUIRE(componentMatches(vectors[3][4], 0.00615991838f));
REQUIRE(componentMatches(vectors[4][4], 0.164715111f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 5; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
SECTION("6x6 Symmetric Eigen") {
Matrix<6, 6> A{4, 1, 0, 0, 0, 1, 1, 5, 1, 0, 0, 0, 0, 1, 6, 1, 0, 0, 0, 0,
1, 7, 1, 0, 0, 0, 0, 1, 8, 1, 1, 0, 0, 0, 1, 3};
Matrix<6, 6> vectors{};
Matrix<6, 1> values{};
A.EigenQR(vectors, values, 10000, 1e-6f);
// eigenvalues (descending)
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(8.86080551f, 1e-4f));
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(7.25410175f, 1e-4f));
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(6.11490774f, 1e-4f));
REQUIRE_THAT(values[3][0], Catch::Matchers::WithinRel(4.88509226f, 1e-4f));
REQUIRE_THAT(values[4][0], Catch::Matchers::WithinRel(3.74589825f, 1e-4f));
REQUIRE_THAT(values[5][0], Catch::Matchers::WithinRel(2.13919425f, 1e-4f));
// eigenvector |components| (sign-invariant)
REQUIRE(componentMatches(vectors[0][0], 0.0430923924f));
REQUIRE(componentMatches(vectors[1][0], 0.0662503168f));
REQUIRE(componentMatches(vectors[2][0], 0.212687209f));
REQUIRE(componentMatches(vectors[3][0], 0.542206466f));
REQUIRE(componentMatches(vectors[4][0], 0.7962538f));
REQUIRE(componentMatches(vectors[5][0], 0.143213451f));
REQUIRE(componentMatches(vectors[0][1], 0.0623276457f));
REQUIRE(componentMatches(vectors[1][1], 0.307613879f));
REQUIRE(componentMatches(vectors[2][1], 0.631065309f));
REQUIRE(componentMatches(vectors[3][1], 0.483806193f));
REQUIRE(componentMatches(vectors[4][1], 0.508129358f));
REQUIRE(componentMatches(vectors[5][1], 0.104793385f));
REQUIRE(componentMatches(vectors[0][2], 0.374228716f));
REQUIRE(componentMatches(vectors[1][2], 0.605694294f));
REQUIRE(componentMatches(vectors[2][2], 0.301064402f));
REQUIRE(componentMatches(vectors[3][2], 0.571099699f));
REQUIRE(componentMatches(vectors[4][2], 0.204411641f));
REQUIRE(componentMatches(vectors[5][2], 0.185764849f));
REQUIRE(componentMatches(vectors[0][3], 0.571099699f));
REQUIRE(componentMatches(vectors[1][3], 0.301064402f));
REQUIRE(componentMatches(vectors[2][3], 0.605694294f));
REQUIRE(componentMatches(vectors[3][3], 0.374228716f));
REQUIRE(componentMatches(vectors[4][3], 0.185764849f));
REQUIRE(componentMatches(vectors[5][3], 0.204411641f));
REQUIRE(componentMatches(vectors[0][4], 0.483806193f));
REQUIRE(componentMatches(vectors[1][4], 0.631065309f));
REQUIRE(componentMatches(vectors[2][4], 0.307613879f));
REQUIRE(componentMatches(vectors[3][4], 0.0623276457f));
REQUIRE(componentMatches(vectors[4][4], 0.104793385f));
REQUIRE(componentMatches(vectors[5][4], 0.508129358f));
REQUIRE(componentMatches(vectors[0][5], 0.542206466f));
REQUIRE(componentMatches(vectors[1][5], 0.212687209f));
REQUIRE(componentMatches(vectors[2][5], 0.0662503168f));
REQUIRE(componentMatches(vectors[3][5], 0.0430923924f));
REQUIRE(componentMatches(vectors[4][5], 0.143213451f));
REQUIRE(componentMatches(vectors[5][5], 0.7962538f));
REQUIRE(isOrthogonal(vectors));
for (uint8_t col = 0; col < 6; col++) {
REQUIRE(eigenResidual(A, values[col][0], eigenColumn(vectors, col)) <
1e-4f);
}
}
}
// ============================================================================
// SVD Tests — Reference values computed via scipy.linalg.svd (Python)
// ============================================================================
/**
* @brief Helper: compute Frobenius norm of a matrix.
*/
template <uint8_t rows, uint8_t columns>
static float frobeniusNorm(const Matrix<rows, columns> &M) {
float sum = 0;
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < columns; j++) {
float v = M.Get(i, j);
sum += v * v;
}
}
return sqrtf(sum);
}
/**
* @brief Helper: compute reconstruction error ||A - UΣVᵀ||_F.
*
* Verifies the fundamental SVD identity A = U × diag(σ) × Vᵀ.
* For non-square matrices, only the first min(rows,cols) singular values
* contribute to the reconstruction.
*/
template <uint8_t rows, uint8_t columns>
static float svdReconstructionError(const Matrix<rows, columns> &A,
const Matrix<rows, columns> &U,
const Matrix<columns, 1> &sigma,
const Matrix<columns, columns> &Vt) {
// Compute U × diag(σ): only first min(rows,cols) columns of U are used
constexpr uint8_t k = (rows < columns) ? rows : columns;
Matrix<rows, columns> USigma{0};
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < k; j++) {
USigma[i][j] = U.Get(i, j) * sigma.Get(j, 0);
}
}
// Compute (UΣ) × Vᵀ: only first k rows of Vt are used
Matrix<rows, columns> UVt{0};
for (uint8_t i = 0; i < rows; i++) {
for (uint8_t j = 0; j < columns; j++) {
float sum = 0;
for (uint8_t p = 0; p < k; p++) {
sum += USigma[i][p] * Vt.Get(p, j);
}
UVt[i][j] = sum;
}
}
// Compute ||A - UVᵀ||_F
Matrix<rows, columns> diff{0};
A.Sub(UVt, diff);
return frobeniusNorm(diff);
}
/**
* @brief Helper: check orthogonality of the first k columns of M.
* Verifies M[:,0:k]ᵀ × M[:,0:k] ≈ I_k.
*/
template <uint8_t rows, uint8_t columns>
static float orthogonalityError(const Matrix<rows, columns> &M) {
constexpr uint8_t k = (rows < columns) ? rows : columns;
// Compute Mᵀ × M (should be I_k in top-left)
Matrix<columns, rows> Mt = M.Transpose();
Matrix<columns, columns> MtM{0};
Mt.Mult(M, MtM);
float err = 0;
for (uint8_t i = 0; i < k; i++) {
for (uint8_t j = 0; j < k; j++) {
float expected = (i == j) ? 1.0f : 0.0f;
err += (MtM.Get(i, j) - expected) * (MtM.Get(i, j) - expected);
}
}
return sqrtf(err);
}
/**
* @brief Helper: check that singular values are sorted in descending order.
*/
template <uint8_t maxCols>
static bool isSortedDescending(const Matrix<maxCols, 1> &sigma, uint8_t count) {
for (uint8_t i = 0; i < count - 1; i++) {
if (sigma.Get(i + 1, 0) > sigma.Get(i, 0) + 1e-6f) {
return false;
}
}
return true;
}
TEST_CASE("SVD: Simple 2x2 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2],[3,4]])
// σ = [5.4649857042, 0.3659661906]
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// Verify singular values (verified with Python scipy.linalg.svd)
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(5.4649857042f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.3659661906f, 1e-4f));
// Verify descending order
REQUIRE(isSortedDescending(sigma, 2));
// Verify U is orthogonal: UᵀU ≈ I
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify Vt is orthogonal: VtVᵀ ≈ I
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
// Verify reconstruction: A ≈ U Σ Vᵀ
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: Symmetric Positive Definite 2x2", "Matrix") {
// Reference: scipy.linalg.svd([[5,3],[3,5]])
// σ = [8.0, 2.0] (eigenvalues since symmetric PD)
Matrix<2, 2> A{5.0f, 3.0f, 3.0f, 5.0f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(8.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
// For symmetric PD matrices, U ≈ V (up to sign)
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: Full-Rank 3x3 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2,3],[4,5,6],[7,8,10]])
// σ = [17.4125051668, 0.8751613501, 0.1968665211]
Matrix<3, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(17.4125051668f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.8751613501f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0),
Catch::Matchers::WithinRel(0.1968665211f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 3));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Rank-Deficient 3x3 Matrix", "Matrix") {
// Reference: scipy.linalg.svd([[1,2,3],[4,5,6],[7,8,9]])
// σ = [16.8481033526, 1.0683695146, ~0] (rank 2)
Matrix<3, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(16.8481033526f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(1.0683695146f, 1e-4f));
// Third singular value should be ~0 (rank deficiency)
REQUIRE(sigma.Get(2, 0) < 1e-3f);
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Diagonal 3x3 Matrix", "Matrix") {
// For a diagonal matrix, σ = diagonal entries, U = V = I
// Row-major init: [10,0,0, 0,5,0, 0,0,2] = diag(10,5,2)
Matrix<3, 3> A{10.0f, 0.0f, 0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 2.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: Tall Matrix (4×3)", "Matrix") {
// Reference: scipy.linalg.svd with full_matrices=False
// σ = [25.4624074360, 1.2906616758, ~0] (rank 2)
Matrix<4, 3> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f,
7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f};
Matrix<4, 3> U{};
Matrix<3, 3> Vt{}; // Vt is always n×n
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(25.4624074360f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(1.2906616758f, 1e-4f));
REQUIRE(sigma.Get(2, 0) < 1e-3f);
// U should be 4×3 with orthonormal columns
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Wide Matrix (3×5)", "Matrix") {
// Reference: scipy.linalg.svd with full_matrices=False
// σ = [35.1272233336, 2.4653966969, ~0] (rank 2)
Matrix<3, 5> A{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f};
Matrix<3, 5> U{};
Matrix<5, 5> Vt{}; // Vt is always n×n
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(35.1272233336f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(2.4653966969f, 1e-4f));
REQUIRE(sigma.Get(2, 0) < 1e-3f);
// Vt should be 5×5 with orthonormal rows (first k)
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: 5×5 Symmetric Tridiagonal", "Matrix") {
// Reference: scipy.linalg.svd for discrete Laplacian-like matrix
// σ = [3.7320508076, 3.0, 2.0, 1.0, 0.2679491924]
Matrix<5, 5> A{2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 2.0f, -1.0f, 0.0f,
0.0f, 0.0f, -1.0f, 2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f,
2.0f, -1.0f, 0.0f, 0.0f, 0.0f, -1.0f, 2.0f};
Matrix<5, 5> U{}, Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(3.7320508076f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(3.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0),
Catch::Matchers::WithinRel(0.2679491924f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: 5×5 Full-Rank Random", "Matrix") {
// Reference: scipy.linalg.svd, np.random.default_rng(7).standard_normal((5,5))
// cond ≈ 11.5
// σ = [3.04651784, 2.22681732, 1.84290662, 1.02101969, 0.264826749]
Matrix<5, 5> A{0.00123015f, 0.298746f, -0.274138f, -0.890592f, -0.454671f,
-0.991647f, 0.0601436f, 1.34022f, -0.492207f, -0.620475f,
0.489842f, 0.356887f, 0.105414f, -0.930468f, -0.0292518f,
0.695303f, -1.34421f, -0.457616f, -1.90122f, -1.28954f,
-1.84174f, -0.235091f, -1.26745f, 0.271264f, 0.156751f};
Matrix<5, 5> U{}, Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(3.04651784f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.22681732f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.84290662f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0),
Catch::Matchers::WithinRel(1.02101969f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(0.264826749f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: 5×5 Rank-Deficient (rank 3)", "Matrix") {
// Reference: scipy.linalg.svd of rng.standard_normal((5,3)) @
// rng.standard_normal((3,5)) — exactly rank 3
// σ = [7.29829771, 2.76487864, 1.57392325, ~1e-16, ~1e-16]
Matrix<5, 5> A{3.46252f, -1.52873f, -0.111526f, 1.28954f, -5.18688f,
-1.34702f, 1.92936f, -0.0410797f, -0.958791f, 0.449623f,
0.844592f, 0.0986352f, 0.408213f, 0.124867f, -2.45393f,
1.34177f, -0.587312f, -1.39847f, 0.580032f, -0.167692f,
0.915462f, -0.311165f, -1.16141f, 0.377283f, 0.055373f};
Matrix<5, 5> U{}, Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(7.29829771f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.76487864f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.57392325f, 1e-4f));
// The two rank-deficient singular values must be at noise level
REQUIRE(sigma.Get(3, 0) < 1e-3f);
REQUIRE(sigma.Get(4, 0) < 1e-3f);
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
// Rank-3 matrix: the top-3 SVD terms must reproduce A
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 5e-3f));
}
TEST_CASE("SVD: 5×5 Wide Dynamic Range (cond ≈ 9000)", "Matrix") {
// Symmetric banded, diagonal decays 50 → 1e-3, off-diagonals 3 → 0.01
// Reference: scipy.linalg.svd
// σ = [50.1496395, 10.1719501, 1.02846061, 0.0207253626, 0.00557535757]
Matrix<5, 5> A{50.0f, 3.0f, 0.0f, 0.0f, 0.0f,
-3.0f, 10.0f, 0.5f, 0.0f, 0.0f,
0.0f, -0.5f, 1.0f, 0.08f, 0.0f,
0.0f, 0.0f, -0.08f, 0.01f, 0.01f,
0.0f, 0.0f, 0.0f, -0.01f, 0.001f};
Matrix<5, 5> U{}, Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(50.1496395f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(10.1719501f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0),
Catch::Matchers::WithinRel(1.02846061f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0),
Catch::Matchers::WithinRel(0.0207253626f, 1e-3f));
REQUIRE_THAT(sigma.Get(4, 0),
Catch::Matchers::WithinRel(0.00557535757f, 1e-3f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
// Relative to the largest entry (‖A‖F ≈ 50.1)
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 5e-2f));
}
TEST_CASE("SVD: 5×5 Symmetric Indefinite", "Matrix") {
// Symmetric with negative eigenvalues — σ must equal |eigenvalues|
// Reference: scipy.linalg.svd
// σ = [4.70141723, 3.76392521, 2.73426097, 1.15773083, 0.642665756]
Matrix<5, 5> A{2.0f, -1.0f, 0.0f, 0.0f, 0.5f,
-1.0f, 2.0f, -1.0f, 0.0f, 0.0f,
0.0f, -1.0f, 3.0f, -1.0f, 0.0f,
0.0f, 0.0f, -1.0f, 2.0f, -1.0f,
0.5f, 0.0f, 0.0f, -1.0f, 4.0f};
Matrix<5, 5> U{}, Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(4.70141723f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(3.76392521f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0),
Catch::Matchers::WithinRel(2.73426097f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0),
Catch::Matchers::WithinRel(1.15773083f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0),
Catch::Matchers::WithinRel(0.642665756f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Non-Square with Negative Values (2×3)", "Matrix") {
// Reference: scipy.linalg.svd([[0.5,-0.3,0.8],[-0.2,0.7,0.1]])
// σ = [1.0384009867, 0.6646227432]
Matrix<2, 3> A{0.5f, -0.3f, 0.8f, -0.2f, 0.7f, 0.1f};
Matrix<2, 3> U{};
Matrix<3, 3> Vt{}; // Vt is always n×n
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0),
Catch::Matchers::WithinRel(1.0384009867f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0),
Catch::Matchers::WithinRel(0.6646227432f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: Near-Singular 2×2 Matrix", "Matrix") {
// Condition number ≈ 1e6 — tests numerical stability
// Reference: scipy.linalg.svd([[1,0],[0,1e-6]])
// σ = [1.0, 1e-6]
Matrix<2, 2> A{1.0f, 0.0f, 0.0f, 1e-6f};
Matrix<2, 2> U{}, Vt{};
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1e-6f, 1e-2f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
TEST_CASE("SVD: Orthogonal Matrix (3×3)", "Matrix") {
// For an orthogonal matrix, all singular values should be 1.
// Rotation matrix about z-axis by 45°
float c = sqrtf(0.5f); // cos(45°)
float s = sqrtf(0.5f); // sin(45°)
Matrix<3, 3> A{c, -s, 0.0f, s, c, 0.0f, 0.0f, 0.0f, 1.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// All singular values should be 1 for an orthogonal matrix
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: Identity Matrix", "Matrix") {
// For I, σ = [1, 1, 1], U = V = I
Matrix<3, 3> A{1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
TEST_CASE("SVD: Zero Matrix", "Matrix") {
// All singular values should be zero
Matrix<3, 3> A{0.0f};
Matrix<3, 3> U{}, Vt{};
Matrix<3, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE(sigma.Get(0, 0) < 1e-6f);
REQUIRE(sigma.Get(1, 0) < 1e-6f);
REQUIRE(sigma.Get(2, 0) < 1e-6f);
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
}
TEST_CASE("SVD: 2×1 Column Vector", "Matrix") {
// For a column vector v, σ = ||v||, U = v/||v|| (with padding)
Matrix<2, 1> A{3.0f, 4.0f};
Matrix<2, 1> U{};
Matrix<1, 1> Vt{}; // Vt is always n×n
Matrix<1, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// σ should be the Euclidean norm: ||[3,4]|| = 5
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
TEST_CASE("SVD: 1×2 Row Vector", "Matrix") {
// For a row vector vᵀ, σ = ||v||, Vt = v/||v|| (with padding)
Matrix<1, 2> A{3.0f, 4.0f};
Matrix<1, 2> U{};
Matrix<2, 2> Vt{}; // Vt is always n×n
Matrix<2, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
// σ should be the Euclidean norm: ||[3,4]|| = 5
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-4f));
}
// ============================================================================
// SVD Tests — Large-Size Instantiations (N > 5)
//
// The SVD is templated on N = max(rows, cols) with stack-only buffers, so
// these cases exercise instantiations beyond the old 5×5 hard limit:
// 7×5 (N=7, tall), 6×6 (N=6, square), 5×8 (N=8, wide/transpose path),
// 6×4 (N=6, tall, near rank-deficiency → deflation path).
// Reference singular values: scipy.linalg.svd.
// ============================================================================
TEST_CASE("SVD: Tall 7×5 Matrix (N=7)", "Matrix") {
// Reference: scipy.linalg.svd
// σ = [7.9180769443, 4.6593687008, 4.2921645616, 2.6009010840, 1.9842770351]
Matrix<7, 5> A{-0.7528f, 2.7043f, 1.392f, 0.592f, -2.0639f,
-2.064f, -2.6515f, 2.1971f, 0.6067f, 1.2484f,
-2.8765f, 2.8195f, 1.9947f, -1.726f, -1.9091f,
-1.8996f, -1.1745f, 0.1485f, -0.4083f, -1.2526f,
0.6711f, -2.163f, -1.2471f, -0.8018f, -0.2636f,
1.7111f, -1.802f, 0.0854f, 0.5545f, -2.7213f,
0.6453f, -1.9769f, -2.6097f, 2.6933f, 2.7938f};
Matrix<7, 5> U{};
Matrix<5, 5> Vt{};
Matrix<5, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(7.9180769443f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.6593687008f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(4.2921645616f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(2.6009010840f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(1.9842770351f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 5));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Square 6×6 Matrix (N=6)", "Matrix") {
// Reference: scipy.linalg.svd (float32 inputs)
// σ = [5.018912792, 4.244967461, 2.505512476,
// 1.838801861, 0.9111995101, 0.4580149353]
Matrix<6, 6> A{1.2336f, -0.7815f, -1.6093f, 0.7369f, -0.2394f, -1.5118f,
-0.0193f, -1.8624f, 1.6373f, -0.9649f, 0.6501f, -0.7532f,
0.0803f, 0.1868f, -1.2606f, 1.8783f,
1.1005f, 1.758f, 1.5793f, 0.3916f, 1.6875f, -1.646f,
-1.2161f, -1.8191f, -0.6987f, -0.4453f, -0.9146f, 1.315f,
-0.573f, -0.8763f, 0.1708f, -1.4363f, 1.2088f, -1.7018f,
1.089f, 1.9475f};
Matrix<6, 6> U{}, Vt{};
Matrix<6, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.018912792f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.244967461f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.505512476f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(1.838801861f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(0.9111995101f, 1e-4f));
REQUIRE_THAT(sigma.Get(5, 0), Catch::Matchers::WithinRel(0.4580149353f, 1e-4f));
REQUIRE(isSortedDescending(sigma, 6));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Wide 5×8 Matrix (N=8, transpose path)", "Matrix") {
// Reference: scipy.linalg.svd
// σ = [5.8027782929, 4.1105282764, 3.7755966048, 3.3208483982, 2.0321410547]
//
// Wide matrices take the Aᵀ transpose path; Vt must be the FULL 8×8
// orthogonal matrix (all 8 rows meaningful), not just the top 5.
Matrix<5, 8> A{-1.5064f, -2.4724f, 1.5773f, 1.0343f, 1.145f, 1.3564f, -2.1298f, -0.7077f,
-1.9207f, 1.8155f, 0.6165f, -0.8455f, -2.1822f, -0.9451f, -0.8741f, 1.148f,
0.6878f, 1.9361f, -0.1389f, -1.902f, 1.0662f, 1.3039f, 0.3064f, 1.3548f,
-0.031f, 0.1137f, -0.3623f, -2.3729f, -1.9605f, -2.3429f, 0.6821f, -0.9282f,
0.0429f, 2.0378f, -1.2535f, -0.4481f, 1.2778f, -1.356f, -2.1151f, -1.0512f};
Matrix<5, 8> U{};
Matrix<8, 8> Vt{};
Matrix<8, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.8027782929f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(4.1105282764f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(3.7755966048f, 1e-4f));
REQUIRE_THAT(sigma.Get(3, 0), Catch::Matchers::WithinRel(3.3208483982f, 1e-4f));
REQUIRE_THAT(sigma.Get(4, 0), Catch::Matchers::WithinRel(2.0321410547f, 1e-4f));
// Remaining singular values must be at noise level
REQUIRE(sigma.Get(5, 0) < 1e-3f);
REQUIRE(sigma.Get(6, 0) < 1e-3f);
REQUIRE(sigma.Get(7, 0) < 1e-3f);
REQUIRE(isSortedDescending(sigma, 8));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}
TEST_CASE("SVD: Tall 6×4 Near Rank-Deficient (N=6, deflation path)", "Matrix") {
// Reference: scipy.linalg.svd
// σ = [5.9434060901, 3.2857910666, 0.3066158795, 6.48e-07]
//
// σ₄ ≈ 6.5e-7 forces the deflation logic to zero the last
// superdiagonal and isolate the trailing 1×1 block.
Matrix<6, 4> A{-0.086904f, 1.410225f, 1.308323f, 2.234762f,
0.022123f, 0.896751f, 0.324176f, 0.773607f,
-0.473015f, 1.555111f, 0.290059f, 1.157726f,
-0.78371f, 1.398884f, -1.930606f, -1.548717f,
0.201518f, -0.626835f, 0.976596f, 0.875294f,
-1.24206f, 1.60595f, -3.078089f, -2.73695f};
Matrix<6, 4> U{};
Matrix<4, 4> Vt{};
Matrix<4, 1> sigma{};
SVD::SVD(A, U, sigma, Vt);
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.9434060901f, 1e-4f));
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(3.2857910666f, 1e-4f));
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(0.3066158795f, 1e-4f));
// Fourth singular value is at noise level (matrix is ~rank 3)
REQUIRE(sigma.Get(3, 0) < 1e-4f);
REQUIRE(isSortedDescending(sigma, 4));
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
REQUIRE_THAT(orthogonalityError(Vt), Catch::Matchers::WithinAbs(0.0f, 1e-3f));
float reconErr = svdReconstructionError(A, U, sigma, Vt);
REQUIRE_THAT(reconErr, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
}