1550 lines
56 KiB
C++
1550 lines
56 KiB
C++
// include the unit test framework first
|
||
#include <catch2/catch_test_macros.hpp>
|
||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||
|
||
// include the module you're going to test next
|
||
#include "Matrix.hpp"
|
||
#include "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));
|
||
}
|