Residual-Adaptive Manifold-Affine Damping / experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, time
  2import numpy as np
  3
  4
  5def rank_project(X, q):
  6    U, s, Vt = np.linalg.svd(X, full_matrices=False)
  7    return (U[:, :q] * s[:q]) @ Vt[:q, :], U[:, :q], Vt[:q, :].T, s
  8
  9
 10def tangent_project(H, U, V):
 11    UtH = U.T @ H
 12    HV = H @ V
 13    UtHV = U.T @ H @ V
 14    return U @ UtH + HV @ V.T - U @ UtHV @ V.T
 15
 16
 17def normal_project(H, U, V):
 18    return H - tangent_project(H, U, V)
 19
 20
 21def adaptive_step(X, B, b, q, c=1.0, rho=1.0, eps=1e-6):
 22    Y, U, V, s = rank_project(X, q)
 23    r = np.linalg.norm(X-Y) / (np.linalg.norm(X) + 1e-12)
 24    mu = c * (r + eps) ** rho
 25    d = X.size
 26    # Explicit Q is appropriate for this deliberately small numerical check.
 27    Q = np.zeros((d, d))
 28    for j in range(d):
 29        E = np.zeros((X.shape[0], X.shape[1])); E.flat[j] = 1.0
 30        Q[:, j] = (normal_project(E, U, V) + mu * E).ravel()
 31    K = np.block([[Q, B.T], [B, np.zeros((B.shape[0], B.shape[0]))]])
 32    rhs = np.concatenate([(mu * X + normal_project(Y, U, V)).ravel(), b])
 33    sol = np.linalg.solve(K, rhs)
 34    Z = sol[:d].reshape(X.shape)
 35    return Z, Y, r, mu
 36
 37
 38def affine_projection(X, B, b):
 39    # Euclidean projection onto B vec(Z)=b.
 40    BBt = B @ B.T
 41    return (X.ravel() + B.T @ np.linalg.solve(BBt, b - B @ X.ravel())).reshape(X.shape)
 42
 43
 44def verify_math(rng):
 45    m, n, q = 7, 6, 2
 46    X = rng.normal(size=(m, n))
 47    Y, U, V, _ = rank_project(X, q)
 48    H = rng.normal(size=(m, n))
 49    PT = tangent_project(H, U, V); PN = normal_project(H, U, V)
 50    projector_err = max(np.linalg.norm(tangent_project(PT, U, V)-PT),
 51                        np.linalg.norm(normal_project(PN, U, V)-PN))
 52    orth_err = abs(np.sum(PT * PN))
 53    decomp_err = np.linalg.norm(PT + PN - H)
 54    # The residual rule should have precisely the claimed power-law log slope.
 55    rs = np.logspace(-6, -1, 20)
 56    mus = np.array([(rs[i] + 1e-12) ** 0.7 for i in range(len(rs))])
 57    slope = np.polyfit(np.log(rs), np.log(mus), 1)[0]
 58    return {"projector_idempotence_error": float(projector_err),
 59            "projector_orthogonality_error": float(orth_err),
 60            "projector_decomposition_error": float(decomp_err),
 61            "mu_log_slope_rho_0.7": float(slope),
 62            "math_pass": bool(projector_err < 1e-10 and orth_err < 1e-10 and decomp_err < 1e-10 and abs(slope-.7)<1e-3)}
 63
 64
 65def run_method(name, W0, target, B, b, q, steps=100, c=1.0, rho=1.0):
 66    W = W0.copy(); hist = []; t0 = time.perf_counter()
 67    for k in range(steps):
 68        # Same gradient step for every method.
 69        X = W - 0.35 * (W - target)
 70        if name == "alternating":
 71            Y, _, _, _ = rank_project(X, q)
 72            W = affine_projection(Y, B, b)
 73            # Keep the standard alternating scheme's next iterate rank feasible.
 74            W, _, _, _ = rank_project(W, q)
 75            mu = None; r = np.linalg.norm(X-Y)/(np.linalg.norm(X)+1e-12)
 76        else:
 77            use_rho = 0.0 if name == "fixed_mu" else rho
 78            Z, Y, r, mu = adaptive_step(X, B, b, q, c=c, rho=use_rho)
 79            W, _, _, _ = rank_project(Z, q)
 80        rank_res = np.linalg.norm(W - rank_project(W, q)[0])/(np.linalg.norm(W)+1e-12)
 81        aff_res = np.linalg.norm(B @ W.ravel() - b)
 82        err = np.linalg.norm(W-target)/np.linalg.norm(target)
 83        hist.append((err, rank_res, aff_res, r, -1 if mu is None else mu))
 84    return {"final_relative_target_error": float(hist[-1][0]),
 85            "final_affine_residual": float(hist[-1][2]),
 86            "final_rank_residual": float(hist[-1][1]),
 87            "initial_affine_residual": float(hist[0][2]),
 88            "min_error_last_20": float(min(x[0] for x in hist[-20:])),
 89            "time_sec": time.perf_counter()-t0,
 90            "history": np.array(hist).tolist()}
 91
 92
 93def main():
 94    rng = np.random.default_rng(284)
 95    math = verify_math(rng)
 96    m, n, q = 8, 7, 2
 97    # A rank-q target defines a consistent affine calibration constraint.
 98    target = rng.normal(size=(m,q)) @ rng.normal(size=(q,n))
 99    d = m*n
100    # Full-row-rank random linear calibration observations.
101    B0 = rng.normal(size=(12,d)); Qb, _ = np.linalg.qr(B0.T); B = Qb[:, :12].T
102    b = B @ target.ravel()
103    W0 = target + 1.8*rng.normal(size=(m,n))
104    configs = {"fixed_mu": (1.0, 0.0), "adaptive_rho05": (1.0, 0.5), "adaptive_rho1": (1.0, 1.0), "adaptive_c10_rho1": (10.0, 1.0), "alternating": (0.0, 0.0)}
105    results = {name: run_method(name, W0, target, B, b, q, c=c, rho=rho) for name, (c, rho) in configs.items()}
106    # Strip histories from summary but save them for reproducibility.
107    out = {"math": math, "benchmark": results,
108           "setup": {"shape": [m,n], "rank": q, "constraints": B.shape[0], "steps": 100,
109                     "learning_rate": .35, "seed": 284}}
110    with open("results.json", "w") as f: json.dump(out, f, indent=2)
111    print(json.dumps({"math": math, "benchmark_summary":
112        {k:{x:v for x,v in val.items() if x != "history"} for k,val in results.items()}}, indent=2))
113
114if __name__ == "__main__": main()