Compare commits
5
Commits
main
...
f12625b41e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f12625b41e | ||
|
|
6f91c96de8 | ||
|
|
5600b05b09 | ||
|
|
a49e357f4c | ||
|
|
ea29ea27f2 |
+1
-1
@@ -4,7 +4,7 @@ project(Vector3D)
|
|||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
add_subdirectory(unit-tests)
|
add_subdirectory(unit-tests)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 11)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
add_compile_options (-fdiagnostics-color=always)
|
add_compile_options (-fdiagnostics-color=always)
|
||||||
|
|||||||
@@ -56,4 +56,21 @@ target_link_libraries(matrix
|
|||||||
set_target_properties(matrix
|
set_target_properties(matrix
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
LINKER_LANGUAGE CXX
|
LINKER_LANGUAGE CXX
|
||||||
|
)
|
||||||
|
|
||||||
|
# SVD
|
||||||
|
add_library(svd
|
||||||
|
STATIC
|
||||||
|
SVD.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(svd
|
||||||
|
PUBLIC
|
||||||
|
vector-3d-intf
|
||||||
|
PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(svd
|
||||||
|
PROPERTIES
|
||||||
|
LINKER_LANGUAGE CXX
|
||||||
)
|
)
|
||||||
+3
-2
@@ -20,7 +20,8 @@ Matrix<rows, columns>::Matrix(const std::array<float, rows * columns> &array) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <uint8_t rows, uint8_t columns>
|
template <uint8_t rows, uint8_t columns>
|
||||||
template <typename... Args>
|
template <typename... Args,
|
||||||
|
std::enable_if_t<(std::is_arithmetic_v<Args> && ...), int>>
|
||||||
Matrix<rows, columns>::Matrix(Args... args) {
|
Matrix<rows, columns>::Matrix(Args... args) {
|
||||||
constexpr uint16_t arraySize{static_cast<uint16_t>(rows) *
|
constexpr uint16_t arraySize{static_cast<uint16_t>(rows) *
|
||||||
static_cast<uint16_t>(columns)};
|
static_cast<uint16_t>(columns)};
|
||||||
@@ -531,7 +532,7 @@ void Matrix<rows, columns>::QRDecomposition(Matrix<rows, columns> &Q,
|
|||||||
Q.Fill(0);
|
Q.Fill(0);
|
||||||
R.Fill(0);
|
R.Fill(0);
|
||||||
Matrix<rows, 1> a_col, e, u, Q_column_k{};
|
Matrix<rows, 1> a_col, e, u, Q_column_k{};
|
||||||
Matrix<1, rows> a_T, e_T{};
|
Matrix<1, rows> e_T{};
|
||||||
|
|
||||||
for (uint8_t column = 0; column < columns; column++) {
|
for (uint8_t column = 0; column < columns; column++) {
|
||||||
this->GetColumn(column, a_col);
|
this->GetColumn(column, a_col);
|
||||||
|
|||||||
+6
-2
@@ -3,6 +3,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
// TODO: Add a function to calculate eigenvalues/vectors
|
// TODO: Add a function to calculate eigenvalues/vectors
|
||||||
// TODO: Add a function to compute RREF
|
// TODO: Add a function to compute RREF
|
||||||
@@ -29,9 +30,12 @@ public:
|
|||||||
Matrix(const Matrix<rows, columns> &other);
|
Matrix(const Matrix<rows, columns> &other);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initialize a matrix directly with any number of arguments
|
* @brief Initialize a matrix directly with scalar values
|
||||||
|
* Uses SFINAE to only accept arithmetic types (int, float, double, etc.)
|
||||||
*/
|
*/
|
||||||
template <typename... Args> Matrix(Args... args);
|
template <typename... Args,
|
||||||
|
std::enable_if_t<(std::is_arithmetic_v<Args> && ...), int> = 0>
|
||||||
|
Matrix(Args... args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Create an identity matrix
|
* @brief Create an identity matrix
|
||||||
|
|||||||
+846
@@ -0,0 +1,846 @@
|
|||||||
|
// This #ifndef section makes clangd happy so that it can properly do type hints
|
||||||
|
// in this file
|
||||||
|
#ifndef SVD_H_
|
||||||
|
#define SVD_H_
|
||||||
|
#include "SVD.hpp"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef SVD_H_ // since the .cpp file has to be included by the .hpp file this
|
||||||
|
// will evaluate to true
|
||||||
|
#include "SVD.hpp"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SVD Building Block Implementations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
float SVD::ComputeHouseholder(const float *x, uint8_t len, float *v,
|
||||||
|
float &alpha) {
|
||||||
|
// Compute ||x||
|
||||||
|
float norm = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
norm += x[i] * x[i];
|
||||||
|
}
|
||||||
|
norm = sqrtf(norm);
|
||||||
|
|
||||||
|
if (norm < 1e-30f) {
|
||||||
|
alpha = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
v[i] = 0.0f;
|
||||||
|
}
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose sign to avoid cancellation: alpha has opposite sign of x[0]
|
||||||
|
alpha = (x[0] >= 0.0f) ? -norm : norm;
|
||||||
|
|
||||||
|
// v = x - alpha * e1, then normalize
|
||||||
|
float v0 = x[0] - alpha;
|
||||||
|
|
||||||
|
// Compute ||v||² directly: v0² + x₁² + ... + xₙ₋₁²
|
||||||
|
float vv = v0 * v0;
|
||||||
|
for (uint8_t i = 1; i < len; i++) {
|
||||||
|
vv += x[i] * x[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vv < 1e-30f) {
|
||||||
|
// Already aligned with e1
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
v[i] = (i == 0) ? 1.0f : 0.0f;
|
||||||
|
}
|
||||||
|
return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
float scale = 1.0f / sqrtf(vv);
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
v[i] = (i == 0) ? v0 * scale : x[i] * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return norm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::ApplyHouseholderLeft(Matrix<5, 5> &W, const float *v,
|
||||||
|
uint8_t startRow, uint8_t endRow) {
|
||||||
|
uint8_t len = endRow - startRow + 1;
|
||||||
|
|
||||||
|
// Compute vᵀv (should be 2.0 for our normalized vectors, but compute
|
||||||
|
// explicitly)
|
||||||
|
float vv = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
vv += v[i] * v[i];
|
||||||
|
}
|
||||||
|
if (vv < 1e-30f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float twoOverVv = 2.0f / vv;
|
||||||
|
|
||||||
|
// W = (I - 2vvᵀ) · W
|
||||||
|
for (uint8_t col = 0; col < 5; col++) {
|
||||||
|
float dot = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
dot += v[i] * W[startRow + i][col];
|
||||||
|
}
|
||||||
|
dot *= twoOverVv;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
W[startRow + i][col] -= dot * v[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::ApplyHouseholderRight(Matrix<5, 5> &W, const float *v,
|
||||||
|
uint8_t startCol, uint8_t endCol) {
|
||||||
|
uint8_t len = endCol - startCol + 1;
|
||||||
|
|
||||||
|
float vv = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
vv += v[i] * v[i];
|
||||||
|
}
|
||||||
|
if (vv < 1e-30f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float twoOverVv = 2.0f / vv;
|
||||||
|
|
||||||
|
// W = W · (I - 2vvᵀ)
|
||||||
|
for (uint8_t row = 0; row < 5; row++) {
|
||||||
|
float dot = 0.0f;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
dot += W[row][startCol + i] * v[i];
|
||||||
|
}
|
||||||
|
dot *= twoOverVv;
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
W[row][startCol + i] -= dot * v[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::unused]] void SVD::ComputeGivens(float x, float y, float &c, float &s) {
|
||||||
|
float r = sqrtf(x * x + y * y);
|
||||||
|
|
||||||
|
if (r < 1e-30f) {
|
||||||
|
c = 1.0f;
|
||||||
|
s = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
c = x / r;
|
||||||
|
s = y / r;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::unused]] void SVD::ApplyGivensLeft(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
|
||||||
|
float s, uint8_t startCol, uint8_t endCol) {
|
||||||
|
// [c s] [row_i] = [new_row_i]
|
||||||
|
// [-s c] [row_j] [new_row_j]
|
||||||
|
for (uint8_t col = startCol; col <= endCol && col < 5; col++) {
|
||||||
|
float t1 = W[i][col];
|
||||||
|
float t2 = W[j][col];
|
||||||
|
W[i][col] = c * t1 + s * t2;
|
||||||
|
W[j][col] = -s * t1 + c * t2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[gnu::unused]] void SVD::ApplyGivensRight(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
|
||||||
|
float s, uint8_t startRow, uint8_t endRow) {
|
||||||
|
// [col_i col_j] · [c -s] = [new_col_i new_col_j]
|
||||||
|
// [s c]
|
||||||
|
for (uint8_t row = startRow; row <= endRow && row < 5; row++) {
|
||||||
|
float t1 = W[row][i];
|
||||||
|
float t2 = W[row][j];
|
||||||
|
W[row][i] = c * t1 + s * t2;
|
||||||
|
W[row][j] = -s * t1 + c * t2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Phase 1: Householder Bidiagonalization
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SVD::Bidiagonalize(Matrix<5, 5> &W,
|
||||||
|
uint8_t m, uint8_t q, uint8_t p,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR) {
|
||||||
|
// Working matrix W is m×q (padded to 5×5).
|
||||||
|
// QL and QR are initialized to identity by the caller.
|
||||||
|
// We reduce W to upper bidiagonal form B using Householder reflections.
|
||||||
|
|
||||||
|
float hhVec[5]; // Householder vector storage
|
||||||
|
|
||||||
|
for (uint8_t k = 0; k < p; k++) {
|
||||||
|
// --- Left Householder on column k, rows k..m-1 ---
|
||||||
|
// Zero out subdiagonal elements below B[k+1][k]
|
||||||
|
{
|
||||||
|
uint8_t len = m - k;
|
||||||
|
if (len <= 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Extract the column segment W[k..k+len-1][k]
|
||||||
|
float x[5];
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
x[i] = W[k + i][k];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute Householder reflector
|
||||||
|
float alpha;
|
||||||
|
SVD::ComputeHouseholder(x, len, hhVec, alpha);
|
||||||
|
|
||||||
|
if (alpha == 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Apply H from left to W: W = H·W (columns k..q-1)
|
||||||
|
SVD::ApplyHouseholderLeft(W, hhVec, k, k + len - 1);
|
||||||
|
|
||||||
|
// Apply H from right to QL: QL = QL · H
|
||||||
|
SVD::ApplyHouseholderRight(QL, hhVec, k, k + len - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Right Householder on row k, columns k+1..q-1 ---
|
||||||
|
// Zero out elements above the first superdiagonal in row k.
|
||||||
|
// The Householder maps [W[k][k+1], ..., W[k][q-1]] to [gamma, 0, ..., 0],
|
||||||
|
// preserving the first superdiagonal element (now gamma) and zeroing the rest.
|
||||||
|
{
|
||||||
|
int len = static_cast<int>(q) - 1 - k;
|
||||||
|
if (len <= 1)
|
||||||
|
continue; // Need at least 2 elements to zero something out
|
||||||
|
|
||||||
|
// Extract the row segment starting from column k+1
|
||||||
|
float x[5];
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
x[i] = W[k][k + 1 + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute Householder reflector
|
||||||
|
float alpha;
|
||||||
|
SVD::ComputeHouseholder(x, len, hhVec, alpha);
|
||||||
|
|
||||||
|
if (alpha == 0.0f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Apply H from right to W: W = W·H (columns k+1..k+len-1)
|
||||||
|
SVD::ApplyHouseholderRight(W, hhVec, k + 1, k + len);
|
||||||
|
|
||||||
|
// Apply H from right to QR: QR = QR · H
|
||||||
|
SVD::ApplyHouseholderRight(QR, hhVec, k + 1, k + len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Phase 2 helpers: block solving of the bidiagonal matrix
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SVD::DeflateBidiagonal(Matrix<5, 5> &W, uint8_t p, float tol) {
|
||||||
|
// Zero out superdiagonal elements that are negligible relative to the
|
||||||
|
// local diagonal scale. This deflates the bidiagonal matrix into
|
||||||
|
// independent unreduced blocks, each of which can be solved on its own.
|
||||||
|
if (p < 2)
|
||||||
|
return;
|
||||||
|
for (uint8_t i = 0; i < p - 1; i++) {
|
||||||
|
float test = fabsf(W[i][i + 1]);
|
||||||
|
float scale = fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]);
|
||||||
|
// Use absolute threshold for small scales to avoid division issues
|
||||||
|
if (test < tol * fmaxf(scale, 1e-10f)) {
|
||||||
|
W[i][i + 1] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SVD::BidiagonalIsDiagonal(const Matrix<5, 5> &W, uint8_t p, float tol) {
|
||||||
|
// True when every superdiagonal element of the p×p bidiagonal matrix
|
||||||
|
// has been reduced to (numerically) zero, i.e. the diagonal holds the
|
||||||
|
// singular values and no unreduced blocks remain.
|
||||||
|
if (p < 2)
|
||||||
|
return true;
|
||||||
|
for (uint8_t i = 0; i < p - 1; i++) {
|
||||||
|
if (fabsf(W.Get(i, i + 1)) > tol * 1e-30f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::SolveBidiagonalBlock2x2(float a, float b, float d, float Ublock[2][2],
|
||||||
|
float Vblock[2][2], float sigma[2]) {
|
||||||
|
// Full SVD of the 2×2 upper-bidiagonal block B = [[a, b], [0, d]]:
|
||||||
|
// B = Ublock · diag(sigma[0], sigma[1]) · Vblockᵀ
|
||||||
|
// where:
|
||||||
|
// - sigma[0] ≥ sigma[1] ≥ 0
|
||||||
|
// - columns of Ublock are the left singular vectors
|
||||||
|
// - columns of Vblock are the right singular vectors (Vblock = scipy Vᵀᵀ)
|
||||||
|
//
|
||||||
|
// Uses eigen-decomposition of BᵀB = [[a², ab], [ab, b²+d²]] (symmetric
|
||||||
|
// 2×2, closed form), then uᵢ = B·vᵢ/σᵢ.
|
||||||
|
|
||||||
|
// Singular values = sqrt of eigenvalues of BᵀB (trace/det closed form)
|
||||||
|
float trace = a * a + b * b + d * d;
|
||||||
|
float det = a * a * d * d;
|
||||||
|
float disc = trace * trace - 4.0f * det;
|
||||||
|
if (disc < 0)
|
||||||
|
disc = 0;
|
||||||
|
float sqrtDisc = sqrtf(disc);
|
||||||
|
float hi = sqrtf((trace + sqrtDisc) / 2.0f);
|
||||||
|
float lo = sqrtf((trace - sqrtDisc) / 2.0f);
|
||||||
|
if (lo > hi) {
|
||||||
|
float tmp = hi;
|
||||||
|
hi = lo;
|
||||||
|
lo = tmp;
|
||||||
|
}
|
||||||
|
sigma[0] = hi;
|
||||||
|
sigma[1] = lo;
|
||||||
|
|
||||||
|
// Right singular vector v1: eigenvector of BᵀB for λ1 = hi².
|
||||||
|
// Null-space vector of (BᵀB − λ1·I) is [ab, λ1 − a²].
|
||||||
|
float a2 = a * a;
|
||||||
|
float ab_val = a * b;
|
||||||
|
float e1x = ab_val;
|
||||||
|
float e1y = hi * hi - a2;
|
||||||
|
float normE1 = sqrtf(e1x * e1x + e1y * e1y);
|
||||||
|
float v1x, v1y;
|
||||||
|
if (normE1 > 1e-30f) {
|
||||||
|
v1x = e1x / normE1;
|
||||||
|
v1y = e1y / normE1;
|
||||||
|
} else {
|
||||||
|
// Degenerate (e.g. b = 0 and |a| ≥ |d|): e₁ is already an eigenvector
|
||||||
|
v1x = 1.0f;
|
||||||
|
v1y = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// v2 is the unit vector orthogonal to v1 (completes the 2D basis)
|
||||||
|
float v2x = -v1y;
|
||||||
|
float v2y = v1x;
|
||||||
|
|
||||||
|
// Vblock columns = right singular vectors
|
||||||
|
Vblock[0][0] = v1x;
|
||||||
|
Vblock[1][0] = v1y;
|
||||||
|
Vblock[0][1] = v2x;
|
||||||
|
Vblock[1][1] = v2y;
|
||||||
|
|
||||||
|
// Ublock columns: uᵢ = B·vᵢ / σᵢ, with a rank-deficiency guard.
|
||||||
|
// When σᵢ ≈ 0, dividing produces inf/NaN; instead fill the U column with
|
||||||
|
// the signed orthogonal complement of the other U column (keeps Ublock
|
||||||
|
// orthogonal, and B·vᵢ ≈ 0 so any unit complement satisfies the SVD).
|
||||||
|
float u1x, u1y, u2x, u2y;
|
||||||
|
if (hi > 1e-30f) {
|
||||||
|
u1x = (a * v1x + b * v1y) / hi;
|
||||||
|
u1y = d * v1y / hi;
|
||||||
|
} else {
|
||||||
|
u1x = 1.0f;
|
||||||
|
u1y = 0.0f;
|
||||||
|
}
|
||||||
|
if (lo > 1e-30f) {
|
||||||
|
u2x = (a * v2x + b * v2y) / lo;
|
||||||
|
u2y = d * v2y / lo;
|
||||||
|
} else {
|
||||||
|
u2x = -u1y;
|
||||||
|
u2y = u1x;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ublock[0][0] = u1x;
|
||||||
|
Ublock[1][0] = u1y;
|
||||||
|
Ublock[0][1] = u2x;
|
||||||
|
Ublock[1][1] = u2y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::JacobiEigenSymmetric(float T[5][5], uint8_t n, float evals[5],
|
||||||
|
float V[5][5]) {
|
||||||
|
// Cyclic Jacobi eigenvalue algorithm on symmetric n×n matrix T (in place).
|
||||||
|
// On return:
|
||||||
|
// - T is (near-)diagonal; its diagonal entries are the eigenvalues
|
||||||
|
// - evals[i] = T[i][i] (unsorted)
|
||||||
|
// - columns of V are the corresponding eigenvectors (V is accumulated
|
||||||
|
// as V ← V·J so that T·V = V·Λ)
|
||||||
|
float jacTol = 1e-10f;
|
||||||
|
|
||||||
|
// V starts as the identity: eigenvector accumulator
|
||||||
|
for (uint8_t i = 0; i < n; i++)
|
||||||
|
for (uint8_t j = 0; j < n; j++)
|
||||||
|
V[i][j] = (i == j) ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
for (uint32_t jacIter = 0; jacIter < 100; jacIter++) {
|
||||||
|
// Check convergence over ALL off-diagonal entries, not just the
|
||||||
|
// tridiagonal band: cyclic Jacobi on a 3x3+ block fills non-band
|
||||||
|
// entries (e.g. T[0][2]) during sweeps, so a band-only test can
|
||||||
|
// declare convergence too early.
|
||||||
|
bool converged = true;
|
||||||
|
for (uint8_t i = 0; i < n - 1 && converged; i++) {
|
||||||
|
for (uint8_t j = i + 1; j < n; j++) {
|
||||||
|
float scale = fabsf(T[i][i]) + fabsf(T[j][j]);
|
||||||
|
if (fabsf(T[i][j]) > jacTol * fmaxf(scale, 1e-30f)) {
|
||||||
|
converged = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (converged)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Cyclic Jacobi: zero out T[p][q] for p < q
|
||||||
|
for (uint8_t p = 0; p < n - 1; p++) {
|
||||||
|
for (uint8_t q = p + 1; q < n; q++) {
|
||||||
|
float tPQ = T[p][q];
|
||||||
|
if (fabsf(tPQ) < jacTol * 1e-30f)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
float tPP = T[p][p];
|
||||||
|
float tQQ = T[q][q];
|
||||||
|
float theta = (tQQ - tPP) / (2.0f * tPQ);
|
||||||
|
float t;
|
||||||
|
if (theta >= 0.0f)
|
||||||
|
t = 1.0f / (theta + sqrtf(1.0f + theta * theta));
|
||||||
|
else
|
||||||
|
t = -1.0f / (-theta + sqrtf(1.0f + theta * theta));
|
||||||
|
|
||||||
|
float c = 1.0f / sqrtf(1.0f + t * t);
|
||||||
|
float s = t * c;
|
||||||
|
|
||||||
|
// Update T
|
||||||
|
T[p][p] = tPP - t * tPQ;
|
||||||
|
T[q][q] = tQQ + t * tPQ;
|
||||||
|
T[p][q] = 0.0f;
|
||||||
|
T[q][p] = 0.0f;
|
||||||
|
|
||||||
|
// Update other elements
|
||||||
|
for (uint8_t k = 0; k < n; k++) {
|
||||||
|
if (k == p || k == q)
|
||||||
|
continue;
|
||||||
|
float tPK = T[k][p];
|
||||||
|
float tQK = T[k][q];
|
||||||
|
T[k][p] = c * tPK - s * tQK;
|
||||||
|
T[p][k] = T[k][p];
|
||||||
|
T[k][q] = s * tPK + c * tQK;
|
||||||
|
T[q][k] = T[k][q];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate eigenvectors
|
||||||
|
for (uint8_t k = 0; k < n; k++) {
|
||||||
|
float vKP = V[k][p];
|
||||||
|
float vKQ = V[k][q];
|
||||||
|
V[k][p] = c * vKP - s * vKQ;
|
||||||
|
V[k][q] = s * vKP + c * vKQ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < n; i++) {
|
||||||
|
evals[i] = fabsf(T[i][i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::ApplyBlockFactorsToAccumulators(uint8_t blockStart, uint8_t blockSize,
|
||||||
|
const float Ublock[5][5],
|
||||||
|
const float Vblock[5][5],
|
||||||
|
uint8_t rowsQL, uint8_t rowsQR,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR) {
|
||||||
|
// Fold the block SVD factors into the accumulated Householder
|
||||||
|
// transformation matrices:
|
||||||
|
// QL[:, blockStart..blockStart+blockSize-1] ← QL[:, ...] · Ublock
|
||||||
|
// (over rows 0..rowsQL−1)
|
||||||
|
// QR[:, blockStart..blockStart+blockSize-1] ← QR[:, ...] · Vblock
|
||||||
|
// (over rows 0..rowsQR−1)
|
||||||
|
//
|
||||||
|
// rowsQL / rowsQR are the meaningful row extents of the accumulators:
|
||||||
|
// for a transposed (wide) problem W = Aᵀ has n rows, so QL carries n
|
||||||
|
// meaningful rows while in the normal case it carries m.
|
||||||
|
|
||||||
|
for (uint8_t j = 0; j < rowsQL; j++) {
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint8_t k = 0; k < blockSize; k++) {
|
||||||
|
sum += QL[j][blockStart + k] * Ublock[k][i];
|
||||||
|
}
|
||||||
|
QL[j][blockStart + i] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t j = 0; j < rowsQR; j++) {
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (uint8_t k = 0; k < blockSize; k++) {
|
||||||
|
sum += QR[j][blockStart + k] * Vblock[k][i];
|
||||||
|
}
|
||||||
|
QR[j][blockStart + i] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SVD::SolveBidiagonalBlockJacobi(Matrix<5, 5> &W, uint8_t blockStart,
|
||||||
|
uint8_t blockSize, uint8_t rowsQL,
|
||||||
|
uint8_t rowsQR, Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR, float tol) {
|
||||||
|
// Full SVD of an unreduced upper-bidiagonal block of size > 2 via
|
||||||
|
// eigen-decomposition of the tridiagonal T = BᵀB:
|
||||||
|
// 1. Snapshot the ORIGINAL block diagonal/superdiagonal from W
|
||||||
|
// 2. Form T = BᵀB (tridiagonal symmetric)
|
||||||
|
// 3. JacobiEigenSymmetric → eigenvalues + eigenvector matrix V
|
||||||
|
// 4. Sort eigenvalues descending, reordering V
|
||||||
|
// 5. Ublock = B_orig · V · Σ⁻¹ (computed from the SNAPSHOT so that
|
||||||
|
// overwriting W's diagonal does not corrupt it)
|
||||||
|
// 6. Fold Ublock/Vblock into QL/QR via ApplyBlockFactorsToAccumulators
|
||||||
|
// 7. Only now write sqrt(eigenvalues) into W's diagonal and zero the
|
||||||
|
// block's superdiagonals
|
||||||
|
(void)tol; // Jacobi convergence tolerance is internal
|
||||||
|
|
||||||
|
// Step 1: snapshot original block values (diagonal d[i], superdiag e[i])
|
||||||
|
float d[5], e[4];
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
d[i] = W[blockStart + i][blockStart + i];
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < blockSize - 1; i++) {
|
||||||
|
e[i] = W[blockStart + i][blockStart + i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: form T = BᵀB (tridiagonal)
|
||||||
|
// T[i][i] = d[i]² + e[i−1]² (e[−1] = 0)
|
||||||
|
// T[i][i+1] = d[i] · e[i]
|
||||||
|
float T[5][5] = {{0}};
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
float diag = d[i] * d[i];
|
||||||
|
if (i > 0) {
|
||||||
|
diag += e[i - 1] * e[i - 1];
|
||||||
|
}
|
||||||
|
T[i][i] = diag;
|
||||||
|
if (i < blockSize - 1) {
|
||||||
|
float off = d[i] * e[i];
|
||||||
|
T[i][i + 1] = off;
|
||||||
|
T[i + 1][i] = off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Jacobi eigenvalue algorithm
|
||||||
|
float evals[5] = {0};
|
||||||
|
float V[5][5] = {{0}};
|
||||||
|
SVD::JacobiEigenSymmetric(T, blockSize, evals, V);
|
||||||
|
|
||||||
|
// Step 4: sort eigenvalues descending, reordering eigenvector columns
|
||||||
|
for (uint8_t i = 0; i < blockSize - 1; i++) {
|
||||||
|
for (uint8_t j = i + 1; j < blockSize; j++) {
|
||||||
|
if (evals[j] > evals[i]) {
|
||||||
|
float tmpE = evals[i];
|
||||||
|
evals[i] = evals[j];
|
||||||
|
evals[j] = tmpE;
|
||||||
|
for (uint8_t k = 0; k < blockSize; k++) {
|
||||||
|
float tmpV = V[k][i];
|
||||||
|
V[k][i] = V[k][j];
|
||||||
|
V[k][j] = tmpV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Ublock = B_orig · V · Σ⁻¹, from the SNAPSHOT values.
|
||||||
|
// Column i of Ublock is u_i = (B_orig · v_i) / σ_i.
|
||||||
|
float Ublock[5][5] = {{0}};
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
float sigmaI = sqrtf(evals[i]);
|
||||||
|
for (uint8_t r = 0; r < blockSize; r++) {
|
||||||
|
float result = d[r] * V[r][i];
|
||||||
|
if (r + 1 < blockSize) {
|
||||||
|
result += e[r] * V[r + 1][i];
|
||||||
|
}
|
||||||
|
Ublock[r][i] = (sigmaI > 1e-30f) ? result / sigmaI : 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: fold the factors into the accumulators
|
||||||
|
SVD::ApplyBlockFactorsToAccumulators(blockStart, blockSize, Ublock, V,
|
||||||
|
rowsQL, rowsQR, QL, QR);
|
||||||
|
|
||||||
|
// Step 7: W last — write singular values onto the diagonal and zero
|
||||||
|
// the block's superdiagonals
|
||||||
|
for (uint8_t i = 0; i < blockSize; i++) {
|
||||||
|
W[blockStart + i][blockStart + i] = sqrtf(evals[i]);
|
||||||
|
if (i < blockSize - 1) {
|
||||||
|
W[blockStart + i][blockStart + i + 1] = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Phase 3: Extract and Sort Singular Values
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SVD::ExtractAndSortSingularValues(Matrix<5, 5> &W,
|
||||||
|
Matrix<5, 1> &sigma,
|
||||||
|
uint8_t p,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR) {
|
||||||
|
// Extract singular values as absolute values of diagonal elements.
|
||||||
|
// If a diagonal element is negative, flip the sign of the corresponding
|
||||||
|
// column in QL to maintain U * Sigma * Vt = A.
|
||||||
|
for (uint8_t i = 0; i < p; i++) {
|
||||||
|
if (W[i][i] < 0.0f) {
|
||||||
|
// Flip sign of column i in QL
|
||||||
|
for (uint8_t k = 0; k < 5; k++) {
|
||||||
|
QL[k][i] = -QL[k][i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sigma[i][0] = fabsf(W[i][i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort singular values in descending order and reorder U, V accordingly
|
||||||
|
for (uint8_t i = 0; i < p - 1; i++) {
|
||||||
|
for (uint8_t j = i + 1; j < p; j++) {
|
||||||
|
if (sigma[j][0] > sigma[i][0]) {
|
||||||
|
// Swap singular values
|
||||||
|
float tmpS = sigma[i][0];
|
||||||
|
sigma[i][0] = sigma[j][0];
|
||||||
|
sigma[j][0] = tmpS;
|
||||||
|
|
||||||
|
// Swap columns of QL
|
||||||
|
for (uint8_t k = 0; k < 5; k++) {
|
||||||
|
float tmpQ = QL[k][i];
|
||||||
|
QL[k][i] = QL[k][j];
|
||||||
|
QL[k][j] = tmpQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap columns of QR
|
||||||
|
for (uint8_t k = 0; k < 5; k++) {
|
||||||
|
float tmpQ = QR[k][i];
|
||||||
|
QR[k][i] = QR[k][j];
|
||||||
|
QR[k][j] = tmpQ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Phase 4: Assemble Final U and Vt Matrices
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void SVD::AssembleUAndVt(uint8_t m, uint8_t n, uint8_t p,
|
||||||
|
bool transposeNeeded,
|
||||||
|
const Matrix<5, 5> &QL,
|
||||||
|
const Matrix<5, 5> &QR,
|
||||||
|
Matrix<5, 5> &U,
|
||||||
|
Matrix<5, 5> &Vt) {
|
||||||
|
// Initialize output matrices to zero
|
||||||
|
for (uint8_t i = 0; i < 5; i++)
|
||||||
|
for (uint8_t j = 0; j < 5; j++) {
|
||||||
|
U[i][j] = 0;
|
||||||
|
Vt[i][j] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Compute Final U and Vt ----
|
||||||
|
// After bidiagonalization, A = QL · W · QRᵀ (QL = product of left
|
||||||
|
// Householders in application order, QR = product of right Householders),
|
||||||
|
// and the block solvers fold the block SVD factors in: QL <- QL·U_block,
|
||||||
|
// QR <- QR·V_block. Hence:
|
||||||
|
//
|
||||||
|
// Non-transposed (m ≥ n): A = (QL)·Σ·(QR)ᵀ
|
||||||
|
// U = QL (first m rows, first p columns)
|
||||||
|
// Vt = QRᵀ (first p rows of the n×n matrix)
|
||||||
|
//
|
||||||
|
// Transposed (wide, m < n): we computed SVD of Aᵀ = QL·W·QRᵀ, so
|
||||||
|
// A = (QR)·Σ·(QL)ᵀ
|
||||||
|
// U = QR (first m rows, first p columns) — NOT QRᵀ
|
||||||
|
// Vt = QLᵀ (the FULL n×n transpose: QL is the left singular-vector
|
||||||
|
// matrix of Aᵀ and has n = rows(W) meaningful rows, so Vt needs all
|
||||||
|
// n rows, not just p)
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < m; i++) {
|
||||||
|
for (uint8_t j = 0; j < n; j++) {
|
||||||
|
if (j < p) {
|
||||||
|
if (transposeNeeded) {
|
||||||
|
U[i][j] = QR.Get(i, j);
|
||||||
|
} else {
|
||||||
|
U[i][j] = QL.Get(i, j);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
U[i][j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < n; i++) {
|
||||||
|
for (uint8_t j = 0; j < n; j++) {
|
||||||
|
if (transposeNeeded) {
|
||||||
|
Vt[i][j] = QL.Get(j, i);
|
||||||
|
} else if (i < p) {
|
||||||
|
Vt[i][j] = QR.Get(j, i);
|
||||||
|
} else {
|
||||||
|
Vt[i][j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SVD Implementation - Golub-Kahan-Reinsch Algorithm
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* @brief SVD for any m×n matrix using Householder bidiagonalization +
|
||||||
|
* implicit QR iteration on the bidiagonal form.
|
||||||
|
*
|
||||||
|
* Given A (m×n), computes U (m×k), Σ (k×k diagonal), Vᵀ (k×n) where
|
||||||
|
* k = min(m,n) and A = U·Σ·Vᵀ.
|
||||||
|
*
|
||||||
|
* We store results as:
|
||||||
|
* - U: Matrix<m, n> — first k columns are meaningful
|
||||||
|
* - sigma: Matrix<n, 1> — first k entries are non-zero singular values
|
||||||
|
* - Vt: Matrix<n, n> — first k rows are meaningful
|
||||||
|
*
|
||||||
|
* For m < n (wide matrices), we work with Aᵀ and swap roles of U and V.
|
||||||
|
*/
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
void SVD::SVD(Matrix<rows, columns> &matrixToDecompose,
|
||||||
|
Matrix<rows, columns> &U, Matrix<columns, 1> &sigma,
|
||||||
|
Matrix<columns, columns> &Vt) {
|
||||||
|
static_assert(rows <= 5 && columns <= 5,
|
||||||
|
"SVD currently supports matrices up to 5×5");
|
||||||
|
|
||||||
|
uint8_t m = rows;
|
||||||
|
uint8_t n = columns;
|
||||||
|
uint8_t p = (m < n) ? m : n; // rank = min(m,n)
|
||||||
|
|
||||||
|
// For wide matrices (m < n), work with Aᵀ instead.
|
||||||
|
// SVD(A) = U·Σ·Vᵀ ⟺ SVD(Aᵀ) = V·Σ·Uᵀ
|
||||||
|
// So if we compute SVD(Aᵀ) = Ũ·Σ·Ṽᵀ, then U = Ṽ and Vt = Ũᵀ.
|
||||||
|
bool transposeNeeded = (m < n);
|
||||||
|
|
||||||
|
// Working matrix: always p×p or larger square
|
||||||
|
Matrix<5, 5> W{0};
|
||||||
|
for (uint8_t i = 0; i < m; i++) {
|
||||||
|
for (uint8_t j = 0; j < n; j++) {
|
||||||
|
float val = matrixToDecompose.Get(i, j);
|
||||||
|
if (transposeNeeded) {
|
||||||
|
W[j][i] = val; // store Aᵀ
|
||||||
|
} else {
|
||||||
|
W[i][j] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After bidiagonalization, W holds the bidiagonal matrix B.
|
||||||
|
// Q_L and Q_R accumulate the Householder transformations.
|
||||||
|
Matrix<5, 5> QL{0}, QR{0};
|
||||||
|
for (uint8_t i = 0; i < 5; i++) {
|
||||||
|
QL[i][i] = 1;
|
||||||
|
QR[i][i] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Phase 1: Householder Bidiagonalization ----
|
||||||
|
// For non-transpose (m ≥ n): W is m×n, bidiagonalize to get B (m×n)
|
||||||
|
// For transpose (m < n): W is n×m (= Aᵀ), bidiagonalize to get B (n×m)
|
||||||
|
// Pass the ACTUAL dimensions of W: Bidiagonalize needs the full row count
|
||||||
|
// so that the last left Householder (k = p-1, len = rowsW - k) folds the
|
||||||
|
// extra rows into the last diagonal element and zeros them out.
|
||||||
|
{
|
||||||
|
uint8_t rowsW = transposeNeeded ? n : m; // rows of W
|
||||||
|
uint8_t colsW = transposeNeeded ? m : n; // columns of W
|
||||||
|
SVD::Bidiagonalize(W, rowsW, colsW, p, QL, QR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Phase 2: QR Iteration on Bidiagonal Matrix -->
|
||||||
|
// W now contains the upper bidiagonal matrix B.
|
||||||
|
// We apply QR iterations to converge superdiagonal elements to zero,
|
||||||
|
// leaving singular values on the diagonal.
|
||||||
|
//
|
||||||
|
// rowsQL / rowsQR are the meaningful row extents of the accumulators.
|
||||||
|
// QL is the LEFT factor of the bidiagonalized working matrix W, so it
|
||||||
|
// carries rowsW = (transposeNeeded ? n : m) meaningful rows: in the wide
|
||||||
|
// (transposed) case Vt = QLᵀ needs ALL n rows, so block factors must be
|
||||||
|
// applied over 0..n−1. QR is only ever read back over its first m rows
|
||||||
|
// (as U), but applying factors over all n rows is harmless and matches
|
||||||
|
// the full-row Householder application in Bidiagonalize.
|
||||||
|
uint8_t rowsQL = transposeNeeded ? n : m;
|
||||||
|
uint8_t rowsQR = n;
|
||||||
|
|
||||||
|
uint32_t maxIter = 1000;
|
||||||
|
float tol = 1e-8f;
|
||||||
|
|
||||||
|
for (uint32_t iter = 0; iter < maxIter; iter++) {
|
||||||
|
// Deflate: zero out negligible SUPERDIAGONAL elements
|
||||||
|
SVD::DeflateBidiagonal(W, p, tol);
|
||||||
|
|
||||||
|
// If all superdiagonal elements are zero, we're done
|
||||||
|
if (SVD::BidiagonalIsDiagonal(W, p, tol))
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Process all unreduced blocks in the matrix
|
||||||
|
bool processedAny = false;
|
||||||
|
uint8_t blockStart = 0;
|
||||||
|
|
||||||
|
while (blockStart < p - 1) {
|
||||||
|
// Find end of current unreduced block
|
||||||
|
uint8_t blockEnd = blockStart;
|
||||||
|
while (blockEnd < p - 1 &&
|
||||||
|
fabsf(W[blockEnd][blockEnd + 1]) >
|
||||||
|
tol * fmaxf(fabsf(W[blockEnd][blockEnd]) +
|
||||||
|
fabsf(W[blockEnd + 1][blockEnd + 1]),
|
||||||
|
1e-10f)) {
|
||||||
|
blockEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// blockStart..blockEnd is an unreduced block of size
|
||||||
|
// (blockEnd - blockStart + 1)
|
||||||
|
uint8_t blockSize = blockEnd - blockStart + 1;
|
||||||
|
|
||||||
|
if (blockSize == 2) {
|
||||||
|
// Handle 2×2 block directly using closed-form solution
|
||||||
|
float Ub2[2][2] = {{0}}, Vb2[2][2] = {{0}};
|
||||||
|
float sig[2] = {0, 0};
|
||||||
|
SVD::SolveBidiagonalBlock2x2(W[blockStart][blockStart],
|
||||||
|
W[blockStart][blockEnd],
|
||||||
|
W[blockEnd][blockEnd], Ub2, Vb2, sig);
|
||||||
|
|
||||||
|
float Ublock[5][5] = {{0}}, Vblock[5][5] = {{0}};
|
||||||
|
for (uint8_t i = 0; i < 2; i++)
|
||||||
|
for (uint8_t j = 0; j < 2; j++) {
|
||||||
|
Ublock[i][j] = Ub2[i][j];
|
||||||
|
Vblock[i][j] = Vb2[i][j];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply block factors to the accumulators
|
||||||
|
SVD::ApplyBlockFactorsToAccumulators(blockStart, 2, Ublock, Vblock,
|
||||||
|
rowsQL, rowsQR, QL, QR);
|
||||||
|
|
||||||
|
// Store singular values on diagonal, zero the superdiagonal
|
||||||
|
W[blockStart][blockStart] = sig[0];
|
||||||
|
W[blockEnd][blockEnd] = sig[1];
|
||||||
|
W[blockStart][blockEnd] = 0;
|
||||||
|
} else if (blockSize > 2) {
|
||||||
|
// Larger blocks: SVD via eigendecomposition of BᵀB (Jacobi)
|
||||||
|
SVD::SolveBidiagonalBlockJacobi(W, blockStart, blockSize, rowsQL,
|
||||||
|
rowsQR, QL, QR, tol);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedAny = true;
|
||||||
|
blockStart = blockEnd + 1; // Move to next block
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!processedAny) {
|
||||||
|
// No unreduced blocks found, but superdiagonal is not all zero
|
||||||
|
// This can happen with numerical issues, just break
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Phase 3: Extract and Sort Singular Values ----
|
||||||
|
// Use internal 5×1 buffer for sigma
|
||||||
|
Matrix<5, 1> sigmaInternal{0};
|
||||||
|
ExtractAndSortSingularValues(W, sigmaInternal, p, QL, QR);
|
||||||
|
|
||||||
|
// ---- Phase 4: Assemble Final U and Vt ----
|
||||||
|
// Use internal 5×5 buffers for U and Vt
|
||||||
|
Matrix<5, 5> UInternal{0}, VtInternal{0};
|
||||||
|
AssembleUAndVt(m, n, p, transposeNeeded, QL, QR, UInternal, VtInternal);
|
||||||
|
|
||||||
|
// Copy results to output parameters
|
||||||
|
for (uint8_t i = 0; i < columns; i++) {
|
||||||
|
sigma[i][0] = sigmaInternal.Get(i, 0);
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < rows; i++) {
|
||||||
|
for (uint8_t j = 0; j < columns; j++) {
|
||||||
|
U[i][j] = UInternal.Get(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vt is a columns×columns (n×n) matrix: BOTH bounds must run over columns.
|
||||||
|
for (uint8_t i = 0; i < columns; i++) {
|
||||||
|
for (uint8_t j = 0; j < columns; j++) {
|
||||||
|
Vt[i][j] = VtInternal.Get(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+345
@@ -0,0 +1,345 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Matrix.hpp"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief library that uses Matrix.hpp and performs SVD on a matrix
|
||||||
|
*/
|
||||||
|
namespace SVD {
|
||||||
|
/**
|
||||||
|
* @brief Compute the Singular Value Decomposition (SVD) of this matrix.
|
||||||
|
*
|
||||||
|
* Decomposes A into U × Σ × Vᵀ where:
|
||||||
|
* - U is an m×k orthogonal matrix (left singular vectors)
|
||||||
|
* - Σ is a k×k diagonal matrix with non-negative singular values
|
||||||
|
* (stored as a k×1 column vector)
|
||||||
|
* - Vᵀ is a k×n orthogonal matrix (right singular vectors, transposed)
|
||||||
|
* - k = min(m, n)
|
||||||
|
*
|
||||||
|
* The decomposition satisfies: A ≈ U × diag(Σ) × Vᵀ
|
||||||
|
* Singular values are returned in descending order.
|
||||||
|
*
|
||||||
|
* @param U Output: left singular vectors (m×k orthogonal matrix)
|
||||||
|
* @param sigma Output: singular values as k×1 column vector, sorted descending
|
||||||
|
* @param Vt Output: right singular vectors transposed (k×n matrix)
|
||||||
|
*
|
||||||
|
* @note This implementation uses the Golub-Kahan-Reinsch algorithm:
|
||||||
|
* 1. Householder bidiagonalization of A
|
||||||
|
* 2. Implicit QR iteration on the bidiagonal matrix
|
||||||
|
* 3. Accumulation of U and V factors throughout
|
||||||
|
*/
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
void SVD(Matrix<rows, columns> &matrixToDecompose, Matrix<rows, columns> &U,
|
||||||
|
Matrix<columns, 1> &sigma, Matrix<columns, columns> &Vt);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// SVD Building Block Functions (for unit testing)
|
||||||
|
// These operate on internal 5×5 working arrays for maximum flexibility.
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compute a Householder reflector vector.
|
||||||
|
*
|
||||||
|
* Given input vector x, computes normalized v and scalar alpha such that:
|
||||||
|
* (I - 2·v·vᵀ) · x = [alpha, 0, 0, ...]ᵀ
|
||||||
|
*
|
||||||
|
* @param x Input vector (up to 5 elements)
|
||||||
|
* @param len Number of valid elements in x
|
||||||
|
* @param v Output: normalized Householder vector (v[0] is the first element)
|
||||||
|
* @param alpha Output: the resulting first element after reflection
|
||||||
|
* @return The norm of the input vector x
|
||||||
|
*/
|
||||||
|
static float ComputeHouseholder(const float *x, uint8_t len, float *v,
|
||||||
|
float &alpha);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply a Householder reflection from the left.
|
||||||
|
*
|
||||||
|
* Transforms W = (I - 2·v·vᵀ) · W where v operates on rows [startRow..endRow].
|
||||||
|
*
|
||||||
|
* @param W Input/output: matrix to transform (5×5 working array)
|
||||||
|
* @param v Householder vector (length = endRow - startRow + 1)
|
||||||
|
* @param startRow First row index
|
||||||
|
* @param endRow Last row index
|
||||||
|
*/
|
||||||
|
static void ApplyHouseholderLeft(Matrix<5, 5> &W, const float *v,
|
||||||
|
uint8_t startRow, uint8_t endRow);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply a Householder reflection from the right.
|
||||||
|
*
|
||||||
|
* Transforms W = W · (I - 2·v·vᵀ) where v operates on columns
|
||||||
|
* [startCol..endCol].
|
||||||
|
*
|
||||||
|
* @param W Input/output: matrix to transform (5×5 working array)
|
||||||
|
* @param v Householder vector (length = endCol - startCol + 1)
|
||||||
|
* @param startCol First column index
|
||||||
|
* @param endCol Last column index
|
||||||
|
*/
|
||||||
|
static void ApplyHouseholderRight(Matrix<5, 5> &W, const float *v,
|
||||||
|
uint8_t startCol, uint8_t endCol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reduce a matrix to upper bidiagonal form using Householder reflections.
|
||||||
|
*
|
||||||
|
* Applies a sequence of Householder reflections to reduce the input matrix
|
||||||
|
* W (m×q, where q ≥ p) to upper bidiagonal form B (p×q), accumulating
|
||||||
|
* the left and right transformation matrices in QL and QR respectively.
|
||||||
|
*
|
||||||
|
* Algorithm (Golub-Kahan bidiagonalization):
|
||||||
|
* For k = 0 to p-1:
|
||||||
|
* 1. Left HH on column k, rows k..m-1: zero out subdiagonal below B[k+1][k]
|
||||||
|
* 2. Right HH on row k, cols k+2..q-1: zero out superdiagonal above B[k][k+1]
|
||||||
|
*
|
||||||
|
* The accumulated transformations satisfy:
|
||||||
|
* QLᵀ · W_original · QR = B (upper bidiagonal)
|
||||||
|
*
|
||||||
|
* @param W Input/output: matrix to bidiagonalize (5×5, must be at least p×q)
|
||||||
|
* @param m Number of rows in the working matrix
|
||||||
|
* @param q Number of columns in the working matrix (q ≥ p)
|
||||||
|
* @param p Rank = min(m, original_columns) — number of bidiagonalization steps
|
||||||
|
* @param QL Input/output: left Householder accumulation (initialized to identity,
|
||||||
|
* output: QLᵀ such that QLᵀ·W = B)
|
||||||
|
* @param QR Input/output: right Householder accumulation (initialized to identity,
|
||||||
|
* output: QR such that W·QR = B after left apply)
|
||||||
|
*/
|
||||||
|
static void Bidiagonalize(Matrix<5, 5> &W,
|
||||||
|
uint8_t m, uint8_t q, uint8_t p,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Deflate a bidiagonal matrix by zeroing negligible superdiagonals.
|
||||||
|
*
|
||||||
|
* Scans the p×p upper-bidiagonal matrix stored in W and zeros out any
|
||||||
|
* superdiagonal element W[i][i+1] whose magnitude is negligible relative to
|
||||||
|
* the local diagonal scale (|W[i][i]| + |W[i+1][i+1]|). Deflating splits
|
||||||
|
* the matrix into independent unreduced blocks that can each be solved
|
||||||
|
* separately.
|
||||||
|
*
|
||||||
|
* @param W Input/output: bidiagonal matrix (5×5 working array, first p×p used)
|
||||||
|
* @param p Size of the bidiagonal matrix (min(rows, columns))
|
||||||
|
* @param tol Relative deflation tolerance (e.g. 1e-8f)
|
||||||
|
*/
|
||||||
|
static void DeflateBidiagonal(Matrix<5, 5> &W, uint8_t p, float tol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check whether a bidiagonal matrix has fully reduced to diagonal.
|
||||||
|
*
|
||||||
|
* Returns true when every superdiagonal element of the p×p bidiagonal
|
||||||
|
* matrix in W is (numerically) zero, i.e. the diagonal entries are the
|
||||||
|
* (unsorted) singular values and no unreduced blocks remain.
|
||||||
|
*
|
||||||
|
* @param W Input: bidiagonal matrix (5×5 working array, first p×p used)
|
||||||
|
* @param p Size of the bidiagonal matrix (min(rows, columns))
|
||||||
|
* @param tol Numerical zero threshold multiplier
|
||||||
|
* @return true when all superdiagonal elements are ~0
|
||||||
|
*/
|
||||||
|
static bool BidiagonalIsDiagonal(const Matrix<5, 5> &W, uint8_t p, float tol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compute the full SVD of a 2×2 upper-bidiagonal block (pure).
|
||||||
|
*
|
||||||
|
* Decomposes B = [[a, b], [0, d]] as:
|
||||||
|
* B = Ublock · diag(sigma[0], sigma[1]) · Vblockᵀ
|
||||||
|
*
|
||||||
|
* Guarantees:
|
||||||
|
* - sigma[0] ≥ sigma[1] ≥ 0 (singular values, from eigenvalues of BᵀB)
|
||||||
|
* - Ublock and Vblock are orthogonal (columns are the left/right
|
||||||
|
* singular vectors respectively; Vblock = scipy's Vᵀᵀ)
|
||||||
|
* - Ublock · diag(sigma) · Vblockᵀ == B (within float tolerance)
|
||||||
|
*
|
||||||
|
* Math: eigenvectors of BᵀB = [[a², ab], [ab, b²+d²]] give the right
|
||||||
|
* singular vectors (v1 = normalize(ab, σ1²−a²) with a safe fallback when
|
||||||
|
* that vector is ~0; v2 = (−v1y, v1x)); left singular vectors are
|
||||||
|
* uᵢ = B·vᵢ/σᵢ with a rank-deficiency guard: when σᵢ ≈ 0 (i.e. ~1e-30),
|
||||||
|
* that U column is filled with the signed orthogonal complement of the
|
||||||
|
* other U column instead of dividing by ~0.
|
||||||
|
*
|
||||||
|
* @param a B[0][0] (first diagonal element)
|
||||||
|
* @param b B[0][1] (superdiagonal element)
|
||||||
|
* @param d B[1][1] (second diagonal element)
|
||||||
|
* @param Ublock Output: 2×2 left singular vectors (columns)
|
||||||
|
* @param Vblock Output: 2×2 right singular vectors (columns)
|
||||||
|
* @param sigma Output: singular values, sigma[0] ≥ sigma[1] ≥ 0
|
||||||
|
*/
|
||||||
|
static void SolveBidiagonalBlock2x2(float a, float b, float d,
|
||||||
|
float Ublock[2][2], float Vblock[2][2],
|
||||||
|
float sigma[2]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cyclic Jacobi eigenvalue algorithm for a symmetric matrix (pure).
|
||||||
|
*
|
||||||
|
* Reduces symmetric n×n matrix T to (near-)diagonal form IN PLACE using
|
||||||
|
* cyclic Jacobi rotations, accumulating the eigenvectors in V.
|
||||||
|
*
|
||||||
|
* On return:
|
||||||
|
* - T's diagonal entries are the eigenvalues (off-diagonals ~0)
|
||||||
|
* - evals[i] = T[i][i], UNSORTED
|
||||||
|
* - columns of V are the corresponding eigenvectors (T·V = V·Λ)
|
||||||
|
*
|
||||||
|
* @param T Input/output: symmetric matrix (5×5 storage, first n×n used,
|
||||||
|
* destroyed in place)
|
||||||
|
* @param n Matrix size (≤ 5)
|
||||||
|
* @param evals Output: eigenvalues, unsorted (evals[i] = T[i][i])
|
||||||
|
* @param V Output: eigenvector matrix, columns are eigenvectors
|
||||||
|
*/
|
||||||
|
static void JacobiEigenSymmetric(float T[5][5], uint8_t n, float evals[5],
|
||||||
|
float V[5][5]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fold a block SVD's factors into the QL/QR accumulators.
|
||||||
|
*
|
||||||
|
* Given the block SVD of a bidiagonal block, B = Ublock·Σ·Vblockᵀ, the
|
||||||
|
* accumulated Householder matrices must absorb the block factors:
|
||||||
|
* QL[:, blockStart..blockStart+blockSize−1] ← QL[:, ...] · Ublock
|
||||||
|
* (rows 0..rowsQL−1)
|
||||||
|
* QR[:, blockStart..blockStart+blockSize−1] ← QR[:, ...] · Vblock
|
||||||
|
* (rows 0..rowsQR−1)
|
||||||
|
*
|
||||||
|
* rowsQL / rowsQR are the meaningful row extents of the accumulators
|
||||||
|
* (e.g. for a wide matrix W = Aᵀ, QL carries n = rows(W) meaningful
|
||||||
|
* rows while QR is read back over its first m rows).
|
||||||
|
*
|
||||||
|
* @param blockStart First column/row index of the block in W
|
||||||
|
* @param blockSize Size of the block (2, or > 2 for the Jacobi path)
|
||||||
|
* @param Ublock Left singular-vector factor of the block (blockSize×blockSize in 5×5 storage)
|
||||||
|
* @param Vblock Right singular-vector factor of the block (blockSize×blockSize in 5×5 storage)
|
||||||
|
* @param rowsQL Number of meaningful rows of QL
|
||||||
|
* @param rowsQR Number of meaningful rows of QR
|
||||||
|
* @param QL Input/output: left transformation accumulator
|
||||||
|
* @param QR Input/output: right transformation accumulator
|
||||||
|
*/
|
||||||
|
static void ApplyBlockFactorsToAccumulators(uint8_t blockStart,
|
||||||
|
uint8_t blockSize,
|
||||||
|
const float Ublock[5][5],
|
||||||
|
const float Vblock[5][5],
|
||||||
|
uint8_t rowsQL, uint8_t rowsQR,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Solve a bidiagonal block larger than 2×2 via Jacobi eigen of BᵀB.
|
||||||
|
*
|
||||||
|
* Computes the full SVD of the unreduced upper-bidiagonal block
|
||||||
|
* W[blockStart..blockStart+blockSize−1] via eigendecomposition of the
|
||||||
|
* tridiagonal T = BᵀB:
|
||||||
|
* 1. Snapshot the ORIGINAL block diagonal/superdiagonal from W
|
||||||
|
* 2. JacobiEigenSymmetric on T → eigenvalues (unsorted) + V
|
||||||
|
* 3. Sort eigenvalues descending, reordering V
|
||||||
|
* 4. Ublock = B_orig · V · Σ⁻¹ (from the snapshot, so W is not
|
||||||
|
* overwritten before Ublock is computed)
|
||||||
|
* 5. Fold Ublock/Vblock into QL/QR via ApplyBlockFactorsToAccumulators
|
||||||
|
* 6. Only then write sqrt(eigenvalues) into W's diagonal and zero the
|
||||||
|
* block's superdiagonals
|
||||||
|
*
|
||||||
|
* @param W Input/output: bidiagonal matrix (5×5 working array); the block's
|
||||||
|
* diagonal holds the singular values and its superdiagonals are
|
||||||
|
* zeroed on return
|
||||||
|
* @param blockStart First column/row index of the block
|
||||||
|
* @param blockSize Size of the block (> 2, ≤ 5)
|
||||||
|
* @param rowsQL Number of meaningful rows of QL
|
||||||
|
* @param rowsQR Number of meaningful rows of QR
|
||||||
|
* @param QL Input/output: left transformation accumulator
|
||||||
|
* @param QR Input/output: right transformation accumulator
|
||||||
|
* @param tol (unused: Jacobi convergence tolerance is internal)
|
||||||
|
*/
|
||||||
|
static void SolveBidiagonalBlockJacobi(Matrix<5, 5> &W, uint8_t blockStart,
|
||||||
|
uint8_t blockSize, uint8_t rowsQL,
|
||||||
|
uint8_t rowsQR, Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR, float tol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Extract singular values from bidiagonal matrix diagonal and sort.
|
||||||
|
*
|
||||||
|
* Extracts absolute values of diagonal elements of W as singular values,
|
||||||
|
* then sorts them in descending order while reordering columns of QL
|
||||||
|
* and QR to maintain consistency.
|
||||||
|
*
|
||||||
|
* @param W Input: bidiagonal matrix (5×5 working array)
|
||||||
|
* @param sigma Output: sorted singular values (5×1 column vector, only first p used)
|
||||||
|
* @param p Number of singular values (min(rows, columns))
|
||||||
|
* @param QL Input/output: left transformation matrix (modified during sort)
|
||||||
|
* @param QR Input/output: right transformation matrix (modified during sort)
|
||||||
|
*/
|
||||||
|
static void ExtractAndSortSingularValues(Matrix<5, 5> &W,
|
||||||
|
Matrix<5, 1> &sigma,
|
||||||
|
uint8_t p,
|
||||||
|
Matrix<5, 5> &QL,
|
||||||
|
Matrix<5, 5> &QR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Assemble final U and Vt matrices from QL/QR.
|
||||||
|
*
|
||||||
|
* Computes the final left singular vectors (U) and right singular vectors
|
||||||
|
* transposed (Vt) from the accumulated Householder transformations.
|
||||||
|
*
|
||||||
|
* For non-transpose case: U = QL[:,0:p], Vt = QR[:,0:p]ᵀ
|
||||||
|
* For transpose case: U = QR[:,0:p]ᵀ, Vt = QL[:,0:p]ᵀ
|
||||||
|
*
|
||||||
|
* @param m Number of rows in original matrix
|
||||||
|
* @param n Number of columns in original matrix
|
||||||
|
* @param p Rank = min(m, n)
|
||||||
|
* @param transposeNeeded True if we computed SVD(Aᵀ) instead of SVD(A)
|
||||||
|
* @param QL Left Householder accumulation (5×5)
|
||||||
|
* @param QR Right Householder accumulation (5×5)
|
||||||
|
* @param U Output: left singular vectors (m×n matrix, only first p columns used)
|
||||||
|
* @param Vt Output: right singular vectors transposed (n×n matrix, only first p rows used)
|
||||||
|
*/
|
||||||
|
static void AssembleUAndVt(uint8_t m, uint8_t n, uint8_t p,
|
||||||
|
bool transposeNeeded,
|
||||||
|
const Matrix<5, 5> &QL,
|
||||||
|
const Matrix<5, 5> &QR,
|
||||||
|
Matrix<5, 5> &U,
|
||||||
|
Matrix<5, 5> &Vt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Compute a Givens rotation that zeros out y.
|
||||||
|
*
|
||||||
|
* Computes c, s such that:
|
||||||
|
* [c s] [x] = [r]
|
||||||
|
* [-s c] [y] [0]
|
||||||
|
* where r = sqrt(x² + y²).
|
||||||
|
*
|
||||||
|
* @param x First element
|
||||||
|
* @param y Second element (to be zeroed)
|
||||||
|
* @param c Output: cosine of rotation angle
|
||||||
|
* @param s Output: sine of rotation angle
|
||||||
|
*/
|
||||||
|
static void ComputeGivens(float x, float y, float &c, float &s);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply a Givens rotation from the left to rows i and j.
|
||||||
|
*
|
||||||
|
* Applies [c s; -s c] to rows i, j of W (columns startCol..endCol).
|
||||||
|
*
|
||||||
|
* @param W Input/output: matrix to transform
|
||||||
|
* @param i First row index
|
||||||
|
* @param j Second row index
|
||||||
|
* @param c Cosine of rotation angle
|
||||||
|
* @param s Sine of rotation angle
|
||||||
|
* @param startCol First column to transform
|
||||||
|
* @param endCol Last column to transform
|
||||||
|
*/
|
||||||
|
static void ApplyGivensLeft(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
|
||||||
|
float s, uint8_t startCol, uint8_t endCol);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Apply a Givens rotation from the right to columns i and j.
|
||||||
|
*
|
||||||
|
* Applies [c -s; s c]ᵀ to columns i, j of W (rows startRow..endRow).
|
||||||
|
*
|
||||||
|
* @param W Input/output: matrix to transform
|
||||||
|
* @param i First column index
|
||||||
|
* @param j Second column index
|
||||||
|
* @param c Cosine of rotation angle
|
||||||
|
* @param s Sine of rotation angle
|
||||||
|
* @param startRow First row to transform
|
||||||
|
* @param endRow Last row to transform
|
||||||
|
*/
|
||||||
|
static void ApplyGivensRight(Matrix<5, 5> &W, uint8_t i, uint8_t j, float c,
|
||||||
|
float s, uint8_t startRow, uint8_t endRow);
|
||||||
|
} // namespace SVD
|
||||||
|
|
||||||
|
#ifndef SVD_H_
|
||||||
|
#include "SVD.cpp"
|
||||||
|
#endif // SVD_H_
|
||||||
@@ -32,4 +32,24 @@ target_link_libraries(vector-3d-tests
|
|||||||
PRIVATE
|
PRIVATE
|
||||||
vector-3d
|
vector-3d
|
||||||
Catch2::Catch2WithMain
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
|
||||||
|
# SVD building block tests
|
||||||
|
add_executable(svd-build-blocks-tests svd-build-blocks-tests.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(svd-build-blocks-tests
|
||||||
|
PRIVATE
|
||||||
|
matrix
|
||||||
|
svd
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
|
||||||
|
# SVD integration tests
|
||||||
|
add_executable(svd-integration-test svd-integration-test.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(svd-integration-test
|
||||||
|
PRIVATE
|
||||||
|
matrix
|
||||||
|
svd
|
||||||
|
Catch2::Catch2WithMain
|
||||||
)
|
)
|
||||||
+401
-5
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
// include the module you're going to test next
|
// include the module you're going to test next
|
||||||
#include "Matrix.hpp"
|
#include "Matrix.hpp"
|
||||||
|
#include "SVD.hpp"
|
||||||
|
|
||||||
// any other libraries
|
// any other libraries
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -389,7 +390,7 @@ TEST_CASE("Identity Matrix", "Matrix") {
|
|||||||
if (oneColumnIndex == column) {
|
if (oneColumnIndex == column) {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||||
} else {
|
} else {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(0.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
oneColumnIndex++;
|
oneColumnIndex++;
|
||||||
@@ -406,7 +407,7 @@ TEST_CASE("Identity Matrix", "Matrix") {
|
|||||||
if (oneColumnIndex == column && row < 3) {
|
if (oneColumnIndex == column && row < 3) {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||||
} else {
|
} else {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(0.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
oneColumnIndex++;
|
oneColumnIndex++;
|
||||||
@@ -422,7 +423,7 @@ TEST_CASE("Identity Matrix", "Matrix") {
|
|||||||
if (oneColumnIndex == column) {
|
if (oneColumnIndex == column) {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinRel(1.0f, 1e-6f));
|
||||||
} else {
|
} else {
|
||||||
REQUIRE_THAT(value, Catch::Matchers::WithinRel(0.0f, 1e-6f));
|
REQUIRE_THAT(value, Catch::Matchers::WithinAbs(0.0f, 1e-6f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
oneColumnIndex++;
|
oneColumnIndex++;
|
||||||
@@ -518,7 +519,7 @@ TEST_CASE("QR Decompositions", "Matrix") {
|
|||||||
// check that all R values are correct
|
// check that all R values are correct
|
||||||
REQUIRE_THAT(R[0][0], Catch::Matchers::WithinRel(3.16228f, 1e-4f));
|
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[0][1], Catch::Matchers::WithinRel(4.42719f, 1e-4f));
|
||||||
REQUIRE_THAT(R[1][0], Catch::Matchers::WithinRel(0.0f, 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));
|
REQUIRE_THAT(R[1][1], Catch::Matchers::WithinRel(0.63246f, 1e-4f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,7 +635,402 @@ TEST_CASE("Eigenvalues and Vectors", "Matrix") {
|
|||||||
REQUIRE_THAT(vectors[1][0], Catch::Matchers::WithinRel(0.525322f, 1e-4f));
|
REQUIRE_THAT(vectors[1][0], Catch::Matchers::WithinRel(0.525322f, 1e-4f));
|
||||||
REQUIRE_THAT(vectors[2][0], Catch::Matchers::WithinRel(0.81867f, 1e-4f));
|
REQUIRE_THAT(vectors[2][0], Catch::Matchers::WithinRel(0.81867f, 1e-4f));
|
||||||
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(-1.11684f, 1e-4f));
|
REQUIRE_THAT(values[0][0], Catch::Matchers::WithinRel(-1.11684f, 1e-4f));
|
||||||
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinRel(0.0f, 1e-4f));
|
REQUIRE_THAT(values[1][0], Catch::Matchers::WithinAbs(0.0f, 1e-4f));
|
||||||
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(16.1168f, 1e-4f));
|
REQUIRE_THAT(values[2][0], Catch::Matchers::WithinRel(16.1168f, 1e-4f));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SVD Tests — Reference values computed via scipy.linalg.svd (Python)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper: compute Frobenius norm of a matrix.
|
||||||
|
*/
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
static float frobeniusNorm(const Matrix<rows, columns> &M) {
|
||||||
|
float sum = 0;
|
||||||
|
for (uint8_t i = 0; i < rows; i++) {
|
||||||
|
for (uint8_t j = 0; j < columns; j++) {
|
||||||
|
float v = M.Get(i, j);
|
||||||
|
sum += v * v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sqrtf(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper: compute reconstruction error ||A - UΣVᵀ||_F.
|
||||||
|
*
|
||||||
|
* Verifies the fundamental SVD identity A = U × diag(σ) × Vᵀ.
|
||||||
|
* For non-square matrices, only the first min(rows,cols) singular values
|
||||||
|
* contribute to the reconstruction.
|
||||||
|
*/
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
static float svdReconstructionError(const Matrix<rows, columns> &A,
|
||||||
|
const Matrix<rows, columns> &U,
|
||||||
|
const Matrix<columns, 1> &sigma,
|
||||||
|
const Matrix<columns, columns> &Vt) {
|
||||||
|
// Compute U × diag(σ): only first min(rows,cols) columns of U are used
|
||||||
|
constexpr uint8_t k = (rows < columns) ? rows : columns;
|
||||||
|
Matrix<rows, columns> USigma{0};
|
||||||
|
for (uint8_t i = 0; i < rows; i++) {
|
||||||
|
for (uint8_t j = 0; j < k; j++) {
|
||||||
|
USigma[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute (UΣ) × Vᵀ: only first k rows of Vt are used
|
||||||
|
Matrix<rows, columns> UVt{0};
|
||||||
|
for (uint8_t i = 0; i < rows; i++) {
|
||||||
|
for (uint8_t j = 0; j < columns; j++) {
|
||||||
|
float sum = 0;
|
||||||
|
for (uint8_t p = 0; p < k; p++) {
|
||||||
|
sum += USigma[i][p] * Vt.Get(p, j);
|
||||||
|
}
|
||||||
|
UVt[i][j] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute ||A - UVᵀ||_F
|
||||||
|
Matrix<rows, columns> diff{0};
|
||||||
|
A.Sub(UVt, diff);
|
||||||
|
return frobeniusNorm(diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper: check orthogonality of the first k columns of M.
|
||||||
|
* Verifies M[:,0:k]ᵀ × M[:,0:k] ≈ I_k.
|
||||||
|
*/
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
static float orthogonalityError(const Matrix<rows, columns> &M) {
|
||||||
|
constexpr uint8_t k = (rows < columns) ? rows : columns;
|
||||||
|
|
||||||
|
// Compute Mᵀ × M (should be I_k in top-left)
|
||||||
|
Matrix<columns, rows> Mt = M.Transpose();
|
||||||
|
Matrix<columns, columns> MtM{0};
|
||||||
|
Mt.Mult(M, MtM);
|
||||||
|
|
||||||
|
float err = 0;
|
||||||
|
for (uint8_t i = 0; i < k; i++) {
|
||||||
|
for (uint8_t j = 0; j < k; j++) {
|
||||||
|
float expected = (i == j) ? 1.0f : 0.0f;
|
||||||
|
err += (MtM.Get(i, j) - expected) * (MtM.Get(i, j) - expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sqrtf(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper: check that singular values are sorted in descending order.
|
||||||
|
*/
|
||||||
|
template <uint8_t maxCols>
|
||||||
|
static bool isSortedDescending(const Matrix<maxCols, 1> &sigma, uint8_t count) {
|
||||||
|
for (uint8_t i = 0; i < count - 1; i++) {
|
||||||
|
if (sigma.Get(i + 1, 0) > sigma.Get(i, 0) + 1e-6f) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD: Simple 2x2 Matrix", "Matrix") {
|
||||||
|
// Reference: scipy.linalg.svd([[1,2],[3,4]])
|
||||||
|
// σ = [5.4649857042, 0.3659661906]
|
||||||
|
Matrix<2, 2> A{1.0f, 2.0f, 3.0f, 4.0f};
|
||||||
|
Matrix<2, 2> U{}, Vt{};
|
||||||
|
Matrix<2, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Verify singular values (verified with Python scipy.linalg.svd)
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0),
|
||||||
|
Catch::Matchers::WithinRel(5.4649857042f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0),
|
||||||
|
Catch::Matchers::WithinRel(0.3659661906f, 1e-4f));
|
||||||
|
|
||||||
|
// Verify descending order
|
||||||
|
REQUIRE(isSortedDescending(sigma, 2));
|
||||||
|
|
||||||
|
// Verify U is orthogonal: UᵀU ≈ I
|
||||||
|
REQUIRE_THAT(orthogonalityError(U), Catch::Matchers::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
|
||||||
|
Matrix<3, 3> A{10.0f, 0.0f, 0.0f, 5.0f, 0.0f, 0.0f, 0.0f, 0.0f, 2.0f};
|
||||||
|
Matrix<3, 3> U{}, Vt{};
|
||||||
|
Matrix<3, 1> sigma{};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-4f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-4f));
|
||||||
|
|
||||||
|
float reconErr = svdReconstructionError(A, U, sigma, Vt);
|
||||||
|
REQUIRE_THAT(reconErr, Catch::Matchers::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: 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));
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,8 @@ TEST_CASE("Timing Tests", "Matrix") {
|
|||||||
|
|
||||||
SECTION("Determinant") {
|
SECTION("Determinant") {
|
||||||
for (uint32_t i{0}; i < 1000000; i++) {
|
for (uint32_t i{0}; i < 1000000; i++) {
|
||||||
float det1 = mat4.Det();
|
float det = mat4.Det();
|
||||||
|
(void)det;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
|||||||
|
#include "Matrix.hpp"
|
||||||
|
#include "SVD.hpp"
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// Generic helper functions for any matrix size
|
||||||
|
template <uint8_t rows, uint8_t columns>
|
||||||
|
static float frobeniusNorm(const Matrix<rows, columns> &M) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int i = 0; i < rows; i++)
|
||||||
|
for (int j = 0; j < columns; j++) {
|
||||||
|
float v = M.Get(i, j);
|
||||||
|
sum += v * v;
|
||||||
|
}
|
||||||
|
return sqrtf(sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <uint8_t n>
|
||||||
|
static bool isOrthogonal(const Matrix<n, n> &M, float tol = 1e-4f) {
|
||||||
|
Matrix<n, n> Mt = M.Transpose();
|
||||||
|
Matrix<n, n> MtM{0};
|
||||||
|
Mt.Mult(M, MtM);
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
for (int j = 0; j < n; j++) {
|
||||||
|
float expected = (i == j) ? 1.0f : 0.0f;
|
||||||
|
if (fabsf(MtM.Get(i, j) - expected) > tol)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: 2x2 [[1,2],[3,4]]", "[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<2, 2> A{1, 2, 3, 4};
|
||||||
|
Matrix<2, 2> U{0};
|
||||||
|
Matrix<2, 1> sigma{0};
|
||||||
|
Matrix<2, 2> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Reference singular values from scipy: [5.464985704219, 0.365966190626]
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(5.4649857f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(0.3659662f, 1e-3f));
|
||||||
|
|
||||||
|
// Check orthogonality of U and Vt (first 2x2 blocks)
|
||||||
|
REQUIRE(isOrthogonal<2>(U));
|
||||||
|
REQUIRE(isOrthogonal<2>(Vt));
|
||||||
|
|
||||||
|
// Check reconstruction: A ≈ U · diag(sigma) · Vt
|
||||||
|
Matrix<2, 2> recon{0};
|
||||||
|
Matrix<2, 2> Usig{0};
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
for (int j = 0; j < 2; j++)
|
||||||
|
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||||
|
|
||||||
|
Usig.Mult(Vt, recon);
|
||||||
|
|
||||||
|
float err = 0.0f;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||||
|
err += diff * diff;
|
||||||
|
}
|
||||||
|
err = sqrtf(err);
|
||||||
|
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||||
|
|
||||||
|
std::cout << "SVD 2x2 [[1,2],[3,4]]:\n";
|
||||||
|
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
|
||||||
|
<< "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: 3x3 diagonal [10,5,2]",
|
||||||
|
"[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<3, 3> A{10, 0, 0, 0, 5, 0, 0, 0, 2};
|
||||||
|
Matrix<3, 3> U{0};
|
||||||
|
Matrix<3, 1> sigma{0};
|
||||||
|
Matrix<3, 3> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Singular values should be [10, 5, 2] (already diagonal)
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(10.0f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(5.0f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
|
||||||
|
|
||||||
|
// U and Vt should be identity (or close) for diagonal matrix
|
||||||
|
float uErr = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||||
|
float vtErr = frobeniusNorm(Vt - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||||
|
REQUIRE_THAT(uErr, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
REQUIRE_THAT(vtErr, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: 3x3 rank-deficient [[1,2,3],[4,5,6],[7,8,9]]",
|
||||||
|
"[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<3, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||||
|
Matrix<3, 3> U{0};
|
||||||
|
Matrix<3, 1> sigma{0};
|
||||||
|
Matrix<3, 3> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Reference: [16.848103352614, 1.068369514555, 0.0]
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(16.8481f, 1e-2f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.06837f, 1e-2f));
|
||||||
|
// Third singular value should be ~0 (rank-deficient)
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
// Check reconstruction
|
||||||
|
Matrix<3, 3> recon{0};
|
||||||
|
Matrix<3, 3> Usig{0};
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||||
|
Usig.Mult(Vt, recon);
|
||||||
|
|
||||||
|
float err = 0.0f;
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||||
|
err += diff * diff;
|
||||||
|
}
|
||||||
|
err = sqrtf(err);
|
||||||
|
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
std::cout << "SVD 3x3 rank-deficient:\n";
|
||||||
|
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||||
|
<< sigma.Get(2, 0) << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: tall 4x3 matrix", "[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<4, 3> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
|
||||||
|
Matrix<4, 3> U{0};
|
||||||
|
Matrix<3, 1> sigma{0};
|
||||||
|
Matrix<3, 3> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Reference: [25.462407436036, 1.290661675761, 0.0]
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(25.4624f, 1e-2f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.29066f, 1e-2f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
// Check reconstruction
|
||||||
|
Matrix<4, 3> recon{0};
|
||||||
|
Matrix<4, 3> Usig{0};
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
for (int j = 0; j < 3; j++)
|
||||||
|
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||||
|
Usig.Mult(Vt, recon);
|
||||||
|
|
||||||
|
float err = 0.0f;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
for (int j = 0; j < 3; j++) {
|
||||||
|
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||||
|
err += diff * diff;
|
||||||
|
}
|
||||||
|
err = sqrtf(err);
|
||||||
|
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
std::cout << "SVD tall 4x3:\n";
|
||||||
|
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||||
|
<< sigma.Get(2, 0) << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: wide 3x5 matrix", "[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<3, 5> A{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
|
||||||
|
Matrix<3, 5> U{0};
|
||||||
|
Matrix<5, 1> sigma{0}; // sigma is columns x 1 = 5x1 for wide matrix
|
||||||
|
Matrix<5, 5> Vt{0}; // Vt is columns x columns = 5x5
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// Reference: [35.127223333575, 2.465396696917, 0.0]
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(35.1272f, 1e-2f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.46540f, 1e-2f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
// Check reconstruction: A (3x5) = U * Sigma * Vt, where U (3x5) has
|
||||||
|
// its meaningful part in the first 3 columns, sigma (5x1) in the
|
||||||
|
// first 3 entries, and Vt (5x5) in its first 3 rows (right
|
||||||
|
// singular vectors as rows). So:
|
||||||
|
// A[i][j] = sum_k U[i][k] * sigma[k] * Vt[k][j]
|
||||||
|
|
||||||
|
float err2 = 0.0f;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
for (int j = 0; j < 5; j++) {
|
||||||
|
float recon_val = 0.0f;
|
||||||
|
for (int k = 0; k < 3; k++) {
|
||||||
|
recon_val += U.Get(i, k) * sigma.Get(k, 0) * Vt.Get(k, j);
|
||||||
|
}
|
||||||
|
float diff = recon_val - A.Get(i, j);
|
||||||
|
err2 += diff * diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err2 = sqrtf(err2);
|
||||||
|
REQUIRE_THAT(err2, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
|
||||||
|
std::cout << "SVD wide 3x5:\n";
|
||||||
|
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0) << ", "
|
||||||
|
<< sigma.Get(2, 0) << "]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: identity 3x3", "[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<3, 3> A{1, 0, 0, 0, 1, 0, 0, 0, 1};
|
||||||
|
Matrix<3, 3> U{0};
|
||||||
|
Matrix<3, 1> sigma{0};
|
||||||
|
Matrix<3, 3> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(2, 0), Catch::Matchers::WithinRel(1.0f, 1e-3f));
|
||||||
|
|
||||||
|
float err = frobeniusNorm(U - Matrix<3, 3>{1, 0, 0, 0, 1, 0, 0, 0, 1});
|
||||||
|
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-2f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("SVD Integration: symmetric positive definite 2x2 [[5,3],[3,5]]",
|
||||||
|
"[Matrix][SVD][Integration]") {
|
||||||
|
Matrix<2, 2> A{5, 3, 3, 5};
|
||||||
|
Matrix<2, 2> U{0};
|
||||||
|
Matrix<2, 1> sigma{0};
|
||||||
|
Matrix<2, 2> Vt{0};
|
||||||
|
|
||||||
|
SVD::SVD(A, U, sigma, Vt);
|
||||||
|
|
||||||
|
// For SPD matrix, singular values = eigenvalues: [8, 2]
|
||||||
|
REQUIRE_THAT(sigma.Get(0, 0), Catch::Matchers::WithinRel(8.0f, 1e-3f));
|
||||||
|
REQUIRE_THAT(sigma.Get(1, 0), Catch::Matchers::WithinRel(2.0f, 1e-3f));
|
||||||
|
|
||||||
|
// Check reconstruction
|
||||||
|
Matrix<2, 2> recon{0};
|
||||||
|
Matrix<2, 2> Usig{0};
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
for (int j = 0; j < 2; j++)
|
||||||
|
Usig[i][j] = U.Get(i, j) * sigma.Get(j, 0);
|
||||||
|
Usig.Mult(Vt, recon);
|
||||||
|
|
||||||
|
float err = 0.0f;
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
float diff = recon.Get(i, j) - A.Get(i, j);
|
||||||
|
err += diff * diff;
|
||||||
|
}
|
||||||
|
err = sqrtf(err);
|
||||||
|
REQUIRE_THAT(err, Catch::Matchers::WithinAbs(0.0f, 1e-3f));
|
||||||
|
|
||||||
|
std::cout << "SVD SPD 2x2 [[5,3],[3,5]]:\n";
|
||||||
|
std::cout << "Sigma: [" << sigma.Get(0, 0) << ", " << sigma.Get(1, 0)
|
||||||
|
<< "]\n";
|
||||||
|
}
|
||||||
@@ -0,0 +1,482 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate reference values for SVD building block unit tests.
|
||||||
|
Run this to verify/implement the C++ SVD implementation against scipy/numpy.
|
||||||
|
|
||||||
|
Usage: python3 svd-reference-values.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.linalg import svd, qr as scipy_qr
|
||||||
|
import json
|
||||||
|
|
||||||
|
def compute_householder(x):
|
||||||
|
"""Compute Householder reflector: H*x = [alpha, 0, 0, ...]^T.
|
||||||
|
|
||||||
|
Returns (v_normalized, alpha) where v is the normalized Householder vector.
|
||||||
|
H = I - 2*v*v^T / (v^T*v)
|
||||||
|
"""
|
||||||
|
x = np.array(x, dtype=np.float64)
|
||||||
|
norm_x = np.linalg.norm(x)
|
||||||
|
|
||||||
|
if norm_x < 1e-30:
|
||||||
|
return x.copy(), 0.0
|
||||||
|
|
||||||
|
alpha = -np.sign(x[0]) * norm_x if x[0] != 0 else -norm_x
|
||||||
|
v = x.copy()
|
||||||
|
v[0] -= alpha
|
||||||
|
v_norm = np.linalg.norm(v)
|
||||||
|
|
||||||
|
if v_norm < 1e-30:
|
||||||
|
return np.zeros_like(x), alpha
|
||||||
|
|
||||||
|
v /= v_norm
|
||||||
|
return v, alpha
|
||||||
|
|
||||||
|
def apply_householder_left(A, v, start_row):
|
||||||
|
"""Apply Householder reflection from the left: A = (I - 2vv^T) @ A.
|
||||||
|
|
||||||
|
v is the normalized Householder vector operating on rows [start_row:].
|
||||||
|
The length of v must match the number of rows affected.
|
||||||
|
"""
|
||||||
|
A = A.copy()
|
||||||
|
k = len(v)
|
||||||
|
|
||||||
|
for col in range(A.shape[1]):
|
||||||
|
dot = np.dot(v, A[start_row:start_row+k, col])
|
||||||
|
A[start_row:start_row+k, col] -= 2.0 * dot * v
|
||||||
|
|
||||||
|
return A
|
||||||
|
|
||||||
|
def apply_householder_right(A, v, start_col):
|
||||||
|
"""Apply Householder reflection from the right: A = A @ (I - 2vv^T).
|
||||||
|
|
||||||
|
v is the normalized Householder vector operating on columns [start_col:].
|
||||||
|
The length of v must match the number of columns affected.
|
||||||
|
"""
|
||||||
|
A = A.copy()
|
||||||
|
k = len(v)
|
||||||
|
|
||||||
|
for row in range(A.shape[0]):
|
||||||
|
dot = np.dot(A[row, start_col:start_col+k], v)
|
||||||
|
A[row, start_col:start_col+k] -= 2.0 * dot * v
|
||||||
|
|
||||||
|
return A
|
||||||
|
|
||||||
|
def compute_givens(x, y):
|
||||||
|
"""Compute Givens rotation that zeros out y.
|
||||||
|
|
||||||
|
Returns (c, s) such that [c s; -s c] @ [x; y] = [r; 0].
|
||||||
|
"""
|
||||||
|
r = np.sqrt(x*x + y*y)
|
||||||
|
if r < 1e-30:
|
||||||
|
return 1.0, 0.0
|
||||||
|
c = x / r
|
||||||
|
s = y / r
|
||||||
|
return c, s
|
||||||
|
|
||||||
|
def apply_givens_left(A, i, j, c, s):
|
||||||
|
"""Apply Givens rotation from the left to rows i and j of A.
|
||||||
|
|
||||||
|
[c s] [row_i]
|
||||||
|
[-s c] @ [row_j] = [new_row_i]
|
||||||
|
[new_row_j]
|
||||||
|
"""
|
||||||
|
A = A.copy()
|
||||||
|
new_i = c * A[i] + s * A[j]
|
||||||
|
new_j = -s * A[i] + c * A[j]
|
||||||
|
A[i] = new_i
|
||||||
|
A[j] = new_j
|
||||||
|
return A
|
||||||
|
|
||||||
|
def apply_givens_right(A, i, j, c, s):
|
||||||
|
"""Apply Givens rotation from the right to columns i and j of A.
|
||||||
|
|
||||||
|
[col_i col_j] @ [c -s] = [new_col_i new_col_j]
|
||||||
|
[s c]
|
||||||
|
"""
|
||||||
|
A = A.copy()
|
||||||
|
new_i = c * A[:, i] + s * A[:, j]
|
||||||
|
new_j = -s * A[:, i] + c * A[:, j]
|
||||||
|
A[:, i] = new_i
|
||||||
|
A[:, j] = new_j
|
||||||
|
return A
|
||||||
|
|
||||||
|
def householder_bidiagonalization(A):
|
||||||
|
"""Full Householder bidiagonalization: A = Q_L @ B @ Q_R^T.
|
||||||
|
|
||||||
|
Returns (B, Q_L, Q_R) where B is upper bidiagonal.
|
||||||
|
"""
|
||||||
|
m, n = A.shape
|
||||||
|
p = min(m, n)
|
||||||
|
|
||||||
|
QL = np.eye(m, dtype=np.float64)
|
||||||
|
QR = np.eye(n, dtype=np.float64)
|
||||||
|
W = A.copy()
|
||||||
|
|
||||||
|
for k in range(p):
|
||||||
|
# Left HH: zero out W[k+1:, k]
|
||||||
|
if k < m - 1:
|
||||||
|
x = W[k+1:, k].copy()
|
||||||
|
v, alpha = compute_householder(x)
|
||||||
|
if np.linalg.norm(v) > 1e-30:
|
||||||
|
W = apply_householder_left(W, v, k + 1)
|
||||||
|
QL = apply_householder_right(QL, v, k + 1)
|
||||||
|
|
||||||
|
# Right HH: zero out W[k, k+2:] (superdiagonal)
|
||||||
|
if k < p - 1 and k + 2 <= n:
|
||||||
|
x = W[k, k+2:].copy()
|
||||||
|
v, alpha = compute_householder(x)
|
||||||
|
if np.linalg.norm(v) > 1e-30:
|
||||||
|
W = apply_householder_right(W, v, k + 2)
|
||||||
|
QR = apply_householder_right(QR, v, k + 2)
|
||||||
|
|
||||||
|
return W, QL, QR
|
||||||
|
|
||||||
|
def implicit_qr_iteration(B, QR_acc):
|
||||||
|
"""Implicit QR iteration on a bidiagonal matrix.
|
||||||
|
|
||||||
|
Returns (Sigma, QR_acc) where Sigma is diagonal with singular values
|
||||||
|
and QR_acc contains the accumulated right transformations.
|
||||||
|
"""
|
||||||
|
m, n = B.shape
|
||||||
|
p = min(m, n)
|
||||||
|
W = B.copy()
|
||||||
|
|
||||||
|
max_iter = 1000
|
||||||
|
tol = 1e-10
|
||||||
|
|
||||||
|
for iteration in range(max_iter):
|
||||||
|
# Deflate negligible subdiagonal elements
|
||||||
|
for i in range(p - 1, 0, -1):
|
||||||
|
if abs(W[i, i-1]) < tol * (abs(W[i-1, i-1]) + abs(W[i, i])):
|
||||||
|
W[i, i-1] = 0.0
|
||||||
|
|
||||||
|
# Find smallest unreduced block [start, end]
|
||||||
|
start = 0
|
||||||
|
for i in range(p - 1):
|
||||||
|
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
|
||||||
|
start = i + 1
|
||||||
|
|
||||||
|
end = p - 1
|
||||||
|
for i in range(p - 2, -1, -1):
|
||||||
|
if abs(W[i+1, i]) >= tol * (abs(W[i, i]) + abs(W[i+1, i+1])):
|
||||||
|
end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if start >= end:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Wilkinson shift from bottom 2x2 corner
|
||||||
|
a, b = W[end-1, end-1], W[end-1, end]
|
||||||
|
c_val, d = W[end, end-1], W[end, end]
|
||||||
|
trace = a + d
|
||||||
|
det = a * d - b * c_val
|
||||||
|
disc = trace**2 - 4 * det
|
||||||
|
|
||||||
|
if disc >= 0:
|
||||||
|
sqrt_disc = np.sqrt(disc)
|
||||||
|
e1, e2 = (trace + sqrt_disc) / 2, (trace - sqrt_disc) / 2
|
||||||
|
shift = e1 if abs(e1 - d) < abs(e2 - d) else e2
|
||||||
|
else:
|
||||||
|
shift = d
|
||||||
|
|
||||||
|
# Implicit QR step using Givens rotations
|
||||||
|
# Process from top to bottom within the block
|
||||||
|
x = W[start, start] - shift
|
||||||
|
y = W[start + 1, start]
|
||||||
|
|
||||||
|
for i in range(start, end):
|
||||||
|
r = np.sqrt(x*x + y*y)
|
||||||
|
if r < 1e-30:
|
||||||
|
x = W[i + 1, i]
|
||||||
|
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
c_rot = x / r
|
||||||
|
s_rot = y / r
|
||||||
|
|
||||||
|
# Apply from left to rows i, i+1 (columns i..n-1)
|
||||||
|
for j in range(i, n):
|
||||||
|
t1, t2 = W[i, j], W[i + 1, j]
|
||||||
|
W[i, j] = c_rot * t1 + s_rot * t2
|
||||||
|
W[i + 1, j] = -s_rot * t1 + c_rot * t2
|
||||||
|
|
||||||
|
# Apply from right to columns i, i+1 (rows 0..i)
|
||||||
|
if i > start:
|
||||||
|
for j in range(i + 1):
|
||||||
|
t1, t2 = W[j, i], W[j, i + 1]
|
||||||
|
W[j, i] = c_rot * t1 + s_rot * t2
|
||||||
|
W[j, i + 1] = -s_rot * t1 + c_rot * t2
|
||||||
|
|
||||||
|
# Accumulate into QR_acc
|
||||||
|
for j in range(QR_acc.shape[0]):
|
||||||
|
t1, t2 = QR_acc[j, i], QR_acc[j, i + 1]
|
||||||
|
QR_acc[j, i] = c_rot * t1 + s_rot * t2
|
||||||
|
QR_acc[j, i + 1] = -s_rot * t1 + c_rot * t2
|
||||||
|
|
||||||
|
# Prepare for next rotation
|
||||||
|
x = W[i + 1, i]
|
||||||
|
y = W[i + 1, i + 1] if i + 2 <= end else 0.0
|
||||||
|
|
||||||
|
return W, QR_acc
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("SVB BUILDING BLOCK REFERENCE VALUES")
|
||||||
|
print("Generated with scipy/numpy for C++ unit test verification")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 1: Householder Vector Computation
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 1: computeHouseholderVector")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
test_vectors = [
|
||||||
|
("2D [1,3]", [1.0, 3.0]),
|
||||||
|
("2D [3,4] (norm=5)", [3.0, 4.0]),
|
||||||
|
("3D [1,2,3]", [1.0, 2.0, 3.0]),
|
||||||
|
("3D [0,0,1]", [0.0, 0.0, 1.0]),
|
||||||
|
("4D [5,-3,2,1]", [5.0, -3.0, 2.0, 1.0]),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, vec in test_vectors:
|
||||||
|
v, alpha = compute_householder(vec)
|
||||||
|
x = np.array(vec)
|
||||||
|
Hx = x - 2 * np.dot(v, x) * v
|
||||||
|
|
||||||
|
print(f"\n{name}:")
|
||||||
|
print(f" Input: {list(x)}")
|
||||||
|
print(f" ||x||: {np.linalg.norm(x):.15f}")
|
||||||
|
print(f" alpha: {alpha:.15f}")
|
||||||
|
print(f" v (normalized): {[round(float(vi), 12) for vi in v]}")
|
||||||
|
print(f" H*x = [alpha,0..]: {[round(float(xi), 12) for xi in Hx]}")
|
||||||
|
print(f" Off-diagonal ~0: {np.allclose(Hx[1:], 0, atol=1e-12)}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 2: Householder Apply Left
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 2: applyHouseholderLeft")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
|
||||||
|
x_col = A_test[1:, 0].copy()
|
||||||
|
v_left, _ = compute_householder(x_col)
|
||||||
|
|
||||||
|
print(f"\nInput matrix:\n{A_test}")
|
||||||
|
print(f"Householder vector (rows 1:3): {[round(float(vi), 12) for vi in v_left]}")
|
||||||
|
|
||||||
|
A_result = apply_householder_left(A_test, v_left, 1)
|
||||||
|
print(f"\nAfter applyHouseholderLeft:\n{A_result}")
|
||||||
|
print(f" A[1,0] = {A_result[1,0]:.2e}, A[2,0] = {A_result[2,0]:.2e} (should be ~0)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 3: Householder Apply Right
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 3: applyHouseholderRight")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
A_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]], dtype=np.float64)
|
||||||
|
x_row = A_test[0, 1:].copy()
|
||||||
|
v_right, _ = compute_householder(x_row)
|
||||||
|
|
||||||
|
print(f"\nInput matrix:\n{A_test}")
|
||||||
|
print(f"Householder vector (cols 1:3): {[round(float(vi), 12) for vi in v_right]}")
|
||||||
|
|
||||||
|
A_result = apply_householder_right(A_test, v_right, 1)
|
||||||
|
print(f"\nAfter applyHouseholderRight:\n{A_result}")
|
||||||
|
print(f" A[0,1] = {A_result[0,1]:.2e}, A[0,2] = {A_result[0,2]:.2e} (should be ~0)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 4: Givens Rotation Computation
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 4: computeGivens")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
givens_tests = [
|
||||||
|
("3-4-5 triangle", 3.0, 4.0),
|
||||||
|
("y already zero", 1.0, 0.0),
|
||||||
|
("x is zero", 0.0, 5.0),
|
||||||
|
("Both negative", -3.0, -4.0),
|
||||||
|
("45 degree case", 1.0, -1.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, x, y in givens_tests:
|
||||||
|
c, s = compute_givens(x, y)
|
||||||
|
result_x = c * x + s * y
|
||||||
|
result_y = -s * x + c * y
|
||||||
|
|
||||||
|
print(f"\n{name}: x={x}, y={y}")
|
||||||
|
print(f" r = {np.sqrt(x*x+y*y):.12f}")
|
||||||
|
print(f" c = {c:.12f}, s = {s:.12f}")
|
||||||
|
print(f" [c s; -s c] @ [x;y] = [{result_x:.2e}, {result_y:.2e}]")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 5: Apply Givens Left/Right
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 5: applyGivensLeft / applyGivensRight")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
A_test = np.array([[3.0, 4.0], [1.0, 2.0]], dtype=np.float64)
|
||||||
|
c, s = compute_givens(3.0, 1.0)
|
||||||
|
|
||||||
|
print(f"\nInput matrix:\n{A_test}")
|
||||||
|
print(f"Givens rotation (rows 0,1): c={c:.12f}, s={s:.12f}")
|
||||||
|
|
||||||
|
A_left = apply_givens_left(A_test, 0, 1, c, s)
|
||||||
|
print(f"\nAfter applyGivensLeft:\n{A_left}")
|
||||||
|
print(f" A[1,0] = {A_left[1,0]:.2e} (should be ~0)")
|
||||||
|
|
||||||
|
A_test = np.array([[3.0, 1.0], [4.0, 2.0]], dtype=np.float64)
|
||||||
|
c, s = compute_givens(3.0, 4.0)
|
||||||
|
|
||||||
|
print(f"\nInput matrix:\n{A_test}")
|
||||||
|
print(f"Givens rotation (cols 0,1): c={c:.12f}, s={s:.12f}")
|
||||||
|
|
||||||
|
A_right = apply_givens_right(A_test, 0, 1, c, s)
|
||||||
|
print(f"\nAfter applyGivensRight:\n{A_right}")
|
||||||
|
print(f" A[0,1] = {A_right[0,1]:.2e} (should be ~0)")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 6: Full Bidiagonalization
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 6: householderBidiagonalization")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
bidiag_tests = [
|
||||||
|
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
|
||||||
|
("3x3 SPD [[5,3],[3,5]]", np.array([[5.0, 3.0], [3.0, 5.0]])),
|
||||||
|
("3x3 diag [[10,0,0],[0,5,0],[0,0,2]]",
|
||||||
|
np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
|
||||||
|
("3x3 full [[1,2,3],[4,5,6],[7,8,10]]",
|
||||||
|
np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]])),
|
||||||
|
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, A in bidiag_tests:
|
||||||
|
B, QL, QR = householder_bidiagonalization(A)
|
||||||
|
m, n = A.shape
|
||||||
|
p = min(m, n)
|
||||||
|
|
||||||
|
print(f"\n{name}:")
|
||||||
|
print(f" Original:\n{A}")
|
||||||
|
print(f"\n Bidiagonal B:\n{B}")
|
||||||
|
print(f" Diagonal: {[round(float(B[i,i]), 10) for i in range(p)]}")
|
||||||
|
print(f" Superdiag: {[round(float(B[i,i+1]), 10) for i in range(min(p-1, n-1))]}")
|
||||||
|
|
||||||
|
recon = QL @ B @ QR.T
|
||||||
|
err = np.linalg.norm(recon - A, 'fro')
|
||||||
|
print(f" ||QL @ B @ QR^T - A||_F = {err:.2e}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 7: Full SVD Reference Values
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 7: Full SVD Reference Values (scipy.linalg.svd)")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
test_matrices = [
|
||||||
|
("Simple 2x2", np.array([[1,2],[3,4]], dtype=np.float64)),
|
||||||
|
("SPD 2x2", np.array([[5,3],[3,5]], dtype=np.float64)),
|
||||||
|
("Full-rank 3x3", np.array([[1,2,3],[4,5,6],[7,8,10]], dtype=np.float64)),
|
||||||
|
("Rank-deficient 3x3", np.array([[1,2,3],[4,5,6],[7,8,9]], dtype=np.float64)),
|
||||||
|
("Diagonal 3x3", np.array([[10,0,0],[0,5,0],[0,0,2]], dtype=np.float64)),
|
||||||
|
("Tall 4x3", np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]], dtype=np.float64)),
|
||||||
|
("Wide 3x5", np.array([[1,2,3,4,5],[6,7,8,9,10],[11,12,13,14,15]], dtype=np.float64)),
|
||||||
|
("Symmetric tri 5x5", np.array([[2,-1,0,0,0],[-1,2,-1,0,0],[0,-1,2,-1,0],[0,0,-1,2,-1],[0,0,0,-1,2]], dtype=np.float64)),
|
||||||
|
("Neg values 2x3", np.array([[0.5,-0.3,0.8],[-0.2,0.7,0.1]], dtype=np.float64)),
|
||||||
|
("Near-singular 2x2", np.array([[1,0],[0,1e-6]], dtype=np.float64)),
|
||||||
|
("Orthogonal 3x3", np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0],
|
||||||
|
[np.sin(np.pi/4), np.cos(np.pi/4), 0],
|
||||||
|
[0, 0, 1]], dtype=np.float64)),
|
||||||
|
("Identity 3x3", np.eye(3)),
|
||||||
|
("Zero 3x3", np.zeros((3,3))),
|
||||||
|
("Col vector 2x1", np.array([[3],[4]], dtype=np.float64)),
|
||||||
|
("Row vector 1x2", np.array([[3,4]], dtype=np.float64)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, A in test_matrices:
|
||||||
|
U, s, Vt = svd(A, full_matrices=False)
|
||||||
|
|
||||||
|
print(f"\n{name}: shape={A.shape}")
|
||||||
|
print(f" Singular values: {[round(float(x), 12) for x in s]}")
|
||||||
|
print(f" U:\n{np.array2string(U, precision=6, floatmode='maxprec_equal')}")
|
||||||
|
print(f" Vt:\n{np.array2string(Vt, precision=6, floatmode='maxprec_equal')}")
|
||||||
|
recon_err = np.linalg.norm(A - U @ np.diag(s) @ Vt, 'fro')
|
||||||
|
print(f" Reconstruction error: {recon_err:.2e}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Test 8: Implicit QR Iteration on Bidiagonal
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("TEST 8: implicitQRIteration")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
qr_tests = [
|
||||||
|
("2x2 [[1,2],[3,4]]", np.array([[1.0, 2.0], [3.0, 4.0]])),
|
||||||
|
("3x3 diag", np.array([[10.0, 0, 0], [0, 5.0, 0], [0, 0, 2.0]])),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, A in qr_tests:
|
||||||
|
B, QL, QR = householder_bidiagonalization(A)
|
||||||
|
Sigma, QR_final = implicit_qr_iteration(B.copy(), QR.copy())
|
||||||
|
|
||||||
|
print(f"\n{name}:")
|
||||||
|
print(f" Bidiagonal B:\n{B}")
|
||||||
|
print(f" After QR iteration (Sigma):\n{Sigma}")
|
||||||
|
print(f" Diagonal entries: {[round(float(Sigma[i,i]), 10) for i in range(min(Sigma.shape))]}")
|
||||||
|
|
||||||
|
# Verify: QL @ Sigma @ QR_final^T ≈ A
|
||||||
|
recon = QL @ Sigma @ QR_final.T
|
||||||
|
err = np.linalg.norm(recon - A, 'fro')
|
||||||
|
print(f" ||QL @ Sigma @ QR^T - A||_F = {err:.2e}")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# JSON output for easy import into C++ tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("JSON OUTPUT (for easy C++ integration)")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
json_data = {}
|
||||||
|
|
||||||
|
# Householder test vectors
|
||||||
|
hh_tests = {}
|
||||||
|
for name, vec in test_vectors:
|
||||||
|
v, alpha = compute_householder(vec)
|
||||||
|
x = np.array(vec)
|
||||||
|
Hx = x - 2 * np.dot(v, x) * v
|
||||||
|
hh_tests[name] = {
|
||||||
|
"input": [float(xi) for xi in x],
|
||||||
|
"norm": float(np.linalg.norm(x)),
|
||||||
|
"alpha": float(alpha),
|
||||||
|
"v_normalized": [round(float(vi), 12) for vi in v],
|
||||||
|
"Hx": [round(float(xi), 12) for xi in Hx],
|
||||||
|
}
|
||||||
|
json_data["householder_vectors"] = hh_tests
|
||||||
|
|
||||||
|
# Full SVD reference values
|
||||||
|
svd_tests = {}
|
||||||
|
for name, A in test_matrices:
|
||||||
|
U, s, Vt = svd(A, full_matrices=False)
|
||||||
|
svd_tests[name] = {
|
||||||
|
"shape": list(A.shape),
|
||||||
|
"singular_values": [round(float(x), 12) for x in s],
|
||||||
|
"U": [[round(float(U[i,j]), 8) for j in range(U.shape[1])] for i in range(U.shape[0])],
|
||||||
|
"Vt": [[round(float(Vt[i,j]), 8) for j in range(Vt.shape[1])] for i in range(Vt.shape[0])],
|
||||||
|
}
|
||||||
|
json_data["svd_reference"] = svd_tests
|
||||||
|
|
||||||
|
print(json.dumps(json_data, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user