Working on breaking up the steps into manageable chunks

This commit is contained in:
2026-08-17 15:33:33 -04:00
parent 6f91c96de8
commit f12625b41e
5 changed files with 1116 additions and 237 deletions
+170
View File
@@ -78,6 +78,176 @@ static void ApplyHouseholderLeft(Matrix<5, 5> &W, const float *v,
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+blockSize1] ← QL[:, ...] · Ublock
* (rows 0..rowsQL1)
* QR[:, blockStart..blockStart+blockSize1] ← QR[:, ...] · Vblock
* (rows 0..rowsQR1)
*
* 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+blockSize1] 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.
*