Iterative — Implementation¶
File Structure¶
iterative/
├── tiny_iterative.h (35 lines — API declarations)
└── tiny_iterative.c (755 lines — implementation)
Dependencies: tiny_eigen.h, tiny_decomp.h, tiny_constants.h, <math.h>.
Implementation Summary¶
| Component | Lines | Key Algorithm |
|---|---|---|
| Operator init helpers | 80 | Dense / Hankel / Block-Hankel / Covariance |
| Arnoldi | 120 | MGS-based Krylov iteration |
| Lanczos | 80 | Tridiagonal symmetric iteration |
| RSVD core | 250 | Random projection + QR + small SVD |
| SVD utilities | 20 | Energy / rank / condition |
| Random number generation | 50 | Box-Muller Gaussian RNG |
Operator Interface¶
Each operator init function creates a tiny_operator_f32_t with matvec and rmatvec callbacks:
// Dense operator: matvec = A*x, rmatvec = A^T*x
tiny_operator_f32_t tiny_dense_operator_init(const float *A, int m, int n, int step)
{
tiny_operator_f32_t op = {m, n, NULL, NULL, NULL};
op.ctx = (void*)&((struct dense_ctx){A, m, n, step});
op.matvec = dense_matvec;
op.rmatvec = dense_rmatvec;
return op;
}
Hankel operator builds an implicit Hankel matrix from a 1D signal. Instead of constructing the full \(m \times n\) matrix (\(O(mn)\) memory), matvec computes \(y_k = \sum_{j=0}^{n-1} s_{k+j} \cdot x_j\) on-the-fly (\(O(m \cdot n)\) time, \(O(1)\) extra memory).
Arnoldi: Core Loop¶
for (int j = 0; j < k; j++) {
// 1. Apply operator to current basis vector
op->matvec(&V[(j)*n], &V[(j+1)*n], op->ctx); // v_{j+1} = A * v_j
// 2. MGS orthogonalize against all previous vectors
for (int i = 0; i <= j; i++) {
float h = dot_product(&V[(j+1)*n], &V[i*n], n); // h_{i,j} = v_i · v_{j+1}
H[i*k + j] = h;
axpy(&V[(j+1)*n], -h, &V[i*n], n); // v_{j+1} -= h * v_i
}
// 3. Normalize
float norm = sqrt(dot_product(&V[(j+1)*n], &V[(j+1)*n], n));
H[(j+1)*k + j] = norm;
if (norm > 0) scale(&V[(j+1)*n], 1.0f / norm, n);
else break; // invariant subspace found
}
The Hessenberg matrix \(H\) is reused from subsequent eigenvalue computation (e.g., finding Ritz values).
Lanczos: Tridiagonal Variant¶
Lanczos exploits symmetry (\(A = A^T\)), producing only \(\alpha\) (diagonal) and \(\beta\) (off-diagonal):
for (int j = 0; j < k; j++) {
// 1. Apply operator
op->matvec(&V[j*n], w, op->ctx);
// 2. α_{j} = v_j · (A·v_j)
alpha[j] = dot_product(&V[j*n], w, n);
// 3. Orthogonalize against v_{j-1} and v_j
if (j > 0) axpy(w, -beta[j-1], &V[(j-1)*n], n);
axpy(w, -alpha[j], &V[j*n], n);
// 4. β_j = ||w||
beta[j] = sqrt(dot_product(w, w, n));
if (beta[j] > 0) copy(&V[(j+1)*n], w, n); scale(&V[(j+1)*n], 1/beta[j], n);
else break;
}
Only 2 storage vectors needed vs Arnoldi's \(k\) vectors — significant savings for large \(n\).
Randomized SVD¶
Input: A (m×n), target rank k, oversampling p, power q
Output: U (m×k), S (k), Vt (k×n)
──────────────────────────────────────────────
1. Ω = randn(n, k+p) // Gaussian random matrix
2. Y = A · Ω // m × (k+p)
3. Y = QR → Q // thin QR
4. B = Q^T · A // (k+p) × n
5. [Ũ, Σ, V] = svd(B) // full SVD of tiny B
6. U = Q · Ũ // reconstruct left singular vectors
Power iteration (step 4 in RSVD):
for (int i = 0; i < q; i++) {
// Y = A * (A^T * Y) — power iteration amplifies dominant singular values
op->rmatvec(Y, temp, op->ctx); // temp = A^T * Y
op->matvec(temp, Y, op->ctx); // Y = A * temp
}
Each \(q\)-pass improves the approximation for singular values that decay slowly.
SVD Utilities¶
All utilities operate on the singular value array \(S\):
// Cumulative energy: energy[i] = sum_{j=0}^{i} S_j^2 / sum(S^2)
void tiny_svd_cumulative_energy_f32(const float *S, int len, float *energy) {
float total = 0;
for (int i = 0; i < len; i++) total += S[i]*S[i];
float acc = 0;
for (int i = 0; i < len; i++) {
acc += S[i]*S[i];
energy[i] = acc / total;
}
}
// Model order selection
int order = tiny_svd_rank_by_energy_f32(S, len, 0.99f); // 99% energy
int rank = tiny_svd_rank_by_threshold_f32(S, len, 1e-3f); // numerical rank
float kappa = tiny_svd_condition_estimate_f32(S, len); // condition number