Spectrally identifiable phaseless recurrent layer / spectral_phaseless_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4from sklearn.linear_model import LogisticRegression
  5from sklearn.model_selection import train_test_split
  6from sklearn.pipeline import make_pipeline
  7from sklearn.preprocessing import StandardScaler
  8from sklearn.metrics import accuracy_score
  9
 10SEED = 1461
 11rng = np.random.default_rng(SEED)
 12
 13
 14def laplacian_path(n):
 15    A = np.zeros((n, n))
 16    for i in range(n - 1):
 17        A[i, i + 1] = A[i + 1, i] = 1.0
 18    return np.diag(A.sum(1)) - A
 19
 20
 21def spectral_data(q):
 22    n = len(q)
 23    H = laplacian_path(n) + np.diag(q)
 24    lam, phi = np.linalg.eigh(H)
 25    S = phi ** 2
 26    sums = np.array([lam[i] + lam[j] for i in range(n) for j in range(i, n)])
 27    diffs = np.array([lam[i] - lam[j] for i in range(n) for j in range(i)])
 28    gap_sum = np.min(np.abs(sums[:, None] - sums[None, :] + np.eye(len(sums)) * 1e9)) if len(sums) > 1 else np.inf
 29    gap_diff = np.min(np.abs(diffs[:, None] - diffs[None, :] + np.eye(len(diffs)) * 1e9)) if len(diffs) > 1 else np.inf
 30    overlap = min(np.max(np.abs(phi[:, i] * phi[:, j])) for i in range(n) for j in range(i))
 31    return H, lam, phi, S, float(gap_sum), float(gap_diff), float(np.linalg.svd(S, compute_uv=False)[-1]), float(overlap)
 32
 33
 34def observations(phi, lam, u, times):
 35    c = phi.T @ u
 36    return np.array([np.abs(phi @ (np.exp(-1j * t * lam) * c)) for t in times]).ravel()
 37
 38
 39def phase_aligned_jacobian(phi, lam, u, times):
 40    # finite differences in 2n real coordinates; remove the global-phase tangent
 41    n = len(u)
 42    x = np.r_[u.real, u.imag]
 43    f0 = observations(phi, lam, u, times)
 44    J = np.empty((len(f0), 2*n))
 45    eps = 2e-6
 46    for k in range(2*n):
 47        xp = x.copy(); xp[k] += eps
 48        xm = x.copy(); xm[k] -= eps
 49        J[:, k] = (observations(phi, lam, xp[:n] + 1j*xp[n:], times) - observations(phi, lam, xm[:n] + 1j*xm[n:], times)) / (2*eps)
 50    # tangent to global phase is (-Im u, Re u), which is exactly a null direction
 51    tangent = np.r_[-u.imag, u.real]
 52    tangent /= np.linalg.norm(tangent)
 53    Q, _ = np.linalg.qr(np.column_stack([tangent, rng.normal(size=(2*n, 2*n-1))]))
 54    B = Q[:, 1:]
 55    # QR above may not preserve tangent as first column due nonorthogonal random columns;
 56    # use a robust null-space basis from SVD instead.
 57    _, _, vh = np.linalg.svd(tangent[None, :])
 58    B = vh[1:].T
 59    return np.linalg.svd(J @ B, compute_uv=False)
 60
 61
 62class SpectralPhaselessEncoder:
 63    """Fixed graph Schrodinger evolution exposed only through coordinate magnitudes."""
 64    def __init__(self, graph_laplacian, potential, dt=1.0, steps=6):
 65        self.H = np.asarray(graph_laplacian, float) + np.diag(np.asarray(potential, float))
 66        self.lam, self.phi = np.linalg.eigh(self.H)
 67        self.dt, self.steps = float(dt), int(steps)
 68        self.U = self.phi @ np.diag(np.exp(-1j * self.dt * self.lam)) @ self.phi.T
 69
 70    def transform(self, h):
 71        x = np.asarray(h, complex).copy()
 72        out = []
 73        for _ in range(self.steps):
 74            out.append(np.abs(x))
 75            x = self.U @ x
 76        return np.concatenate(out)
 77
 78    def diagnostics(self):
 79        n = len(self.lam)
 80        sums = np.array([self.lam[i]+self.lam[j] for i in range(n) for j in range(i,n)])
 81        gap = np.min(np.abs(sums[:,None]-sums[None,:] + np.eye(len(sums))*1e9))
 82        S = self.phi**2
 83        overlap = min(np.max(np.abs(self.phi[:,i]*self.phi[:,j])) for i in range(n) for j in range(i))
 84        return {"sigma_min_S": float(np.linalg.svd(S, compute_uv=False)[-1]),
 85                "pair_sum_gap": float(gap), "minimum_pair_overlap": float(overlap)}
 86
 87
 88def math_checks(n=6):
 89    rows = []
 90    for scale in [0.02, 0.1, 0.3, 1.0, 3.0]:
 91        vals = []
 92        for _ in range(40):
 93            q = scale * rng.normal(size=n)
 94            *_, gap_sum, gap_diff, smin, overlap = spectral_data(q)
 95            vals.append((gap_sum, gap_diff, smin, overlap))
 96        a = np.array(vals)
 97        rows.append({"potential_scale": scale, "median_pair_sum_gap": float(np.median(a[:,0])),
 98                     "median_difference_gap": float(np.median(a[:,1])),
 99                     "median_sigma_min_S": float(np.median(a[:,2])),
100                     "median_overlap": float(np.median(a[:,3]))})
101    q = rng.normal(size=n)
102    _, lam, phi, S, gs, gd, sm, ov = spectral_data(q)
103    u = rng.normal(size=n) + 1j*rng.normal(size=n)
104    ranks = []
105    mins = []
106    for K in range(1, 9):
107        times = np.linspace(0, 7.0, K)
108        sv = phase_aligned_jacobian(phi, lam, u, times)
109        ranks.append(int(np.sum(sv > 1e-5 * sv[0])))
110        mins.append(float(sv[-1]))
111    # Noise amplification: perturb observations and solve a local least-squares correction.
112    noise_rows = []
113    for K in [1, 2, 3, 4, 6, 8]:
114        times = np.linspace(0, 7.0, K)
115        sv = phase_aligned_jacobian(phi, lam, u, times)
116        noise_rows.append({"K": K, "jacobian_rank": int(np.sum(sv > 1e-5*sv[0])),
117                           "smallest_nonphase_singular": float(sv[-1]),
118                           "predicted_noise_gain_1_over_sigma": float(1/sv[-1])})
119    return {"selected_operator": {"eigenvalues": lam.tolist(), "pair_sum_gap": gs,
120             "difference_gap": gd, "sigma_min_S": sm, "overlap_min": ov},
121            "conditioning_sweep": rows, "time_sample_sweep": {"K": list(range(1,9)),
122             "local_rank": ranks, "smallest_singular": mins}, "noise_sweep": noise_rows, "global_phase_max_abs_error": float(max(np.max(np.abs(observations(phi, lam, u, np.linspace(0,7,6)) - observations(phi, lam, u*np.exp(1j*1.234), np.linspace(0,7,6)))), 0.0))}
123
124
125def classification(n=6, N=2400):
126    q = rng.normal(size=n)
127    _, lam, phi, *_ = spectral_data(q)
128    # Label is a phase-invariant spectral energy contrast; random global phases cannot affect it.
129    U = rng.normal(size=(N,n)) + 1j*rng.normal(size=(N,n))
130    c = U @ phi
131    label = ((np.abs(c[:,0])**2 + np.abs(c[:,1])**2) > (np.abs(c[:,2])**2 + np.abs(c[:,3])**2)).astype(int)
132    phase = rng.uniform(0, 2*np.pi, N)
133    U *= np.exp(1j*phase)[:,None]
134    def enc(K):
135        ts = np.linspace(0, 7.0, K)
136        return np.concatenate([np.abs((phi @ (np.exp(-1j*t*lam)[:,None] * (U @ phi).T)).T) for t in ts], axis=1)
137    Xidea = enc(6)
138    Xbase = np.abs(U)
139    tr, te = train_test_split(np.arange(N), test_size=.3, random_state=SEED, stratify=label)
140    def fit(X):
141        clf = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000, random_state=SEED))
142        clf.fit(X[tr], label[tr]); return accuracy_score(label[te], clf.predict(X[te]))
143    # Add sensor noise at test time, matching the requested robustness check.
144    noise = .05 * rng.normal(size=Xidea[te].shape)
145    return {"clean_accuracy": {"phaseless_K6": float(fit(Xidea)), "single_time_coordinate_magnitude": float(fit(Xbase))},
146            "noisy_test_accuracy_phaseless_K6": float(accuracy_score(label[te], make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000, random_state=SEED)).fit(Xidea[tr], label[tr]).predict(Xidea[te] + noise))),
147            "N": N, "train_test": [len(tr), len(te)]}
148
149
150if __name__ == "__main__":
151    out = {"seed": SEED, "math": math_checks(), "classification": classification()}
152    Path("results.json").write_text(json.dumps(out, indent=2))
153    print(json.dumps(out, indent=2))