// 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 // ============================================================================ // 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]; } } } 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; } 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; } } 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; } } // ============================================================================ // 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 — first k columns are meaningful * - sigma: Matrix — first k entries are non-zero singular values * - Vt: Matrix — first k rows are meaningful * * For m < n (wide matrices), we work with Aᵀ and swap roles of U and V. */ template void SVD::SVD(Matrix &matrixToDecompose, Matrix &U, Matrix &sigma, Matrix &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 ---- // Reduce W to upper bidiagonal form using Householder reflections. // For a p×q matrix (p ≤ q after transpose), we do p steps: // Step k: zero out subdiagonal in column k, then superdiagonal in row k float hhVec[5]; // Householder vector storage for (uint8_t k = 0; k < p; k++) { // --- Left Householder on column k, rows k..min(m,n)-1 --- { uint8_t len = (m > n) ? m - k : n - k; if (len <= 1) continue; // Extract the column segment float x[5]; for (uint8_t i = 0; i < len; i++) { x[i] = W[k + i][k]; } // Compute Householder reflection: H·x = [α, 0, ..., 0]ᵀ float norm = 0; for (uint8_t i = 0; i < len; i++) norm += x[i] * x[i]; norm = sqrtf(norm); if (norm < 1e-30f) continue; float alpha = (x[0] >= 0) ? -norm : norm; float v0 = x[0] - alpha; float vv = v0 * v0 + norm * norm - alpha * x[0]; if (vv < 1e-30f) continue; float scale = 1.0f / sqrtf(vv); // Store Householder vector (first element is implicit 1, rest in hhVec) hhVec[0] = v0 * scale; // this is the first element of the reflected vector for (uint8_t i = 1; i < len; i++) { hhVec[i] = x[i] * scale; } // Apply H from left to W: W = H·W // For each column j, w[k+i][j] -= 2·v_i·(vᵀ·w_col) / (vᵀv) float vvNorm = 1.0f + hhVec[0] * hhVec[0]; for (uint8_t i = 1; i < len; i++) { vvNorm += hhVec[i] * hhVec[i]; } for (uint8_t j = k; j < n; j++) { float dot = 0; for (uint8_t i = 0; i < len; i++) { dot += hhVec[i] * W[k + i][j]; } dot *= 2.0f / vvNorm; for (uint8_t i = 0; i < len; i++) { W[k + i][j] -= dot * hhVec[i]; } } // Apply H from right to QL: QL = QL · H for (uint8_t j = k; j < m; j++) { float dot = 0; for (uint8_t i = 0; i < len; i++) { dot += hhVec[i] * QL[j][k + i]; } dot *= 2.0f / vvNorm; for (uint8_t i = 0; i < len; i++) { QL[j][k + i] -= dot * hhVec[i]; } } } // --- Right Householder on row k, columns k+1..min(m,n)-1 --- { uint8_t len = (p > 1) ? p - 1 - k : 0; if (len <= 0) continue; // Extract the row segment float x[5]; for (uint8_t i = 0; i < len; i++) { x[i] = W[k][k + 1 + i]; } // Compute Householder reflection float norm = 0; for (uint8_t i = 0; i < len; i++) norm += x[i] * x[i]; norm = sqrtf(norm); if (norm < 1e-30f) continue; float alpha = (x[0] >= 0) ? -norm : norm; float v0 = x[0] - alpha; float vv = v0 * v0 + norm * norm - alpha * x[0]; if (vv < 1e-30f) continue; float scale = 1.0f / sqrtf(vv); hhVec[0] = v0 * scale; for (uint8_t i = 1; i < len; i++) { hhVec[i] = x[i] * scale; } // Compute vᵀv float vvNorm = 1.0f + hhVec[0] * hhVec[0]; for (uint8_t i = 1; i < len; i++) { vvNorm += hhVec[i] * hhVec[i]; } // Apply H from right to W: W = W·H for (uint8_t i = 0; i < m; i++) { float dot = 0; for (uint8_t j = 0; j < len; j++) { dot += hhVec[j] * W[i][k + 1 + j]; } dot *= 2.0f / vvNorm; for (uint8_t j = 0; j < len; j++) { W[i][k + 1 + j] -= dot * hhVec[j]; } } // Apply H from right to QR: QR = QR · H for (uint8_t i = 0; i < n; i++) { float dot = 0; for (uint8_t j = 0; j < len; j++) { dot += hhVec[j] * QR[i][k + 1 + j]; } dot *= 2.0f / vvNorm; for (uint8_t j = 0; j < len; j++) { QR[i][k + 1 + j] -= dot * hhVec[j]; } } } } // ---- Phase 2: Implicit QR Iteration on Bidiagonal Matrix ---- // W now contains the upper bidiagonal matrix B. // We apply implicit QR steps to diagonalize it. uint32_t maxIter = 1000; float tol = 1e-8f; for (uint32_t iter = 0; iter < maxIter; iter++) { // Deflate: zero out negligible subdiagonal elements for (uint8_t i = p - 1; i > 0; i--) { float test = fabsf(W[i][i - 1]); float scale = fabsf(W[i - 1][i - 1]) + fabsf(W[i][i]); if (test < tol * (scale + 1e-30f)) { W[i][i - 1] = 0; } } // Find the smallest unreduced block [start..end] uint8_t start = 0, end = p - 1; for (uint8_t i = 0; i < p - 1; i++) { if (fabsf(W[i + 1][i]) > tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) { start = i + 1; } } for (int8_t i = (int8_t)p - 2; i >= 0; i--) { if (fabsf(W[i + 1][i]) > tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) { end = (uint8_t)i; break; } } // Check convergence of the block if (start >= end) { continue; } bool blockConverged = true; for (uint8_t i = start; i <= end; i++) { if (i > start && fabsf(W[i][i - 1]) > tol * (fabsf(W[i - 1][i - 1]) + fabsf(W[i][i]) + 1e-30f)) { blockConverged = false; break; } if (i < end && fabsf(W[i][i + 1]) > tol * (fabsf(W[i][i]) + fabsf(W[i + 1][i + 1]) + 1e-30f)) { blockConverged = false; break; } } if (blockConverged) continue; // Wilkinson shift from bottom 2×2 corner float a = W[end - 1][end - 1]; float b = W[end - 1][end]; float c = W[end][end - 1]; float d = W[end][end]; float trace = a + d; float det = a * d - b * c; float disc = trace * trace - 4.0f * det; float shift; if (disc >= 0) { float sqrtDisc = sqrtf(disc); float e1 = (trace + sqrtDisc) / 2.0f; float e2 = (trace - sqrtDisc) / 2.0f; shift = (fabsf(e1 - d) < fabsf(e2 - d)) ? e1 : e2; } else { shift = d; } // --- Implicit QR step using Givens rotations --- // First, apply Givens rotation from the left to zero out (W[start][start-1] // - shift) For the bidiagonal structure, we process from top to bottom. float x = W[start][start] - shift; float y = (start > 0) ? W[start][start - 1] : 0.0f; for (uint8_t i = start; i <= end; i++) { float r = sqrtf(x * x + y * y); if (r < 1e-30f) { x = W[i][i]; y = (i < end) ? W[i + 1][i] : 0.0f; continue; } float cs = x / r; float sn = y / r; // Apply Givens from left to rows i, i+1 of W (columns i..p-1) for (uint8_t j = i; j < p; j++) { float t1 = W[i][j]; float t2 = W[i + 1][j]; W[i][j] = cs * t1 + sn * t2; W[i + 1][j] = -sn * t1 + cs * t2; } // Apply Givens from right to columns i, i+1 of W (rows 0..i) if (i > start) { for (uint8_t j = 0; j <= i; j++) { float t1 = W[j][i]; float t2 = W[j][i + 1]; W[j][i] = cs * t1 + sn * t2; W[j][i + 1] = -sn * t1 + cs * t2; } } // Accumulate right transformations into QR for (uint8_t j = 0; j < n; j++) { float t1 = QR[j][i]; float t2 = QR[j][i + 1]; QR[j][i] = cs * t1 + sn * t2; QR[j][i + 1] = -sn * t1 + cs * t2; } // Prepare next Givens rotation x = W[i + 1][i]; y = (i + 1 < end) ? W[i + 1][i + 1] : 0.0f; } } // ---- Phase 3: Extract Results ---- // Singular values are the absolute values of diagonal elements of W for (uint8_t i = 0; i < p; 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]) { 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: Compute Final U and Vt ---- // If transposeNeeded (wide matrix), we computed SVD(Aᵀ) = Ũ·Σ·Ṽᵀ // Then U = Ṽ (= QR[:,0:p]) and Vt = Ũᵀ (= QL[:,0:p]ᵀ) // Otherwise, SVD(A) = QL[:,0:p] · Σ · (QR[:,0:p])ᵀ // So U = QL[:,0:p] and Vt = QR[:,0:p]ᵀ for (uint8_t i = 0; i < m; i++) { for (uint8_t j = 0; j < n; j++) { if (j < p) { if (transposeNeeded) { // U = QR[:, 0:p]ᵀ → U[i][j] = QR[j][i] U[i][j] = QR[j][i]; } else { // U = QL[:, 0:p] U[i][j] = QL[i][j]; } } else { U[i][j] = 0; } } } for (uint8_t i = 0; i < n; i++) { for (uint8_t j = 0; j < m; j++) { if (i < p && j < m) { if (transposeNeeded) { // Vt = QL[:, 0:p]ᵀ → Vt[i][j] = QL[j][i] Vt[i][j] = QL[j][i]; } else { // Vt = QR[:, 0:p]ᵀ → Vt[i][j] = QR[j][i] Vt[i][j] = QR[j][i]; } } else { Vt[i][j] = 0; } } } } #endif