#!/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()