"""Redesigned tests: remove the two confounds in the first pass. Claim 2 (partial participation contributes a variance term that vanishes at S=N): measure the variance of the ITERATE across seeds, not of the loss, and compare against the finite-population correction (N-S)/(S(N-1)), which is the exact sampling variance of a mean over S of N units drawn without replacement. S=N is reported separately as an exact-zero check (no fit). Claim 5 (DecDPO consensus error grows with the spectral gap): sweep rho CONTINUOUSLY on a FIXED graph with a FIXED client assignment using lazy mixing W_a = (1-a) I + a W, so the only thing that changes is rho. Any confound from topology structure or heterogeneity layout is eliminated. """ import json import numpy as np from dpo_exp import make_clients, grad, metropolis, D RESULTS = {} def fed_iterate(cl, R, S, lr=0.5, E=5, seed=0): N = len(cl); rng = np.random.default_rng(seed) th = np.zeros(D) for _ in range(R): sel = rng.choice(N, size=S, replace=False) ds = [] for i in sel: loc = th.copy() for _ in range(E): loc -= lr * grad(loc, cl[i][0], cl[i][1]) ds.append(loc - th) th = th + np.mean(ds, axis=0) return th def claim2(): N, R = 20, 120 cl, _ = make_clients(N, 0.8, seed=1) rows = [] for S in (2, 4, 5, 10, 20): TH = np.array([fed_iterate(cl, R, S, seed=1000 + s) for s in range(24)]) var = float(np.trace(np.cov(TH.T))) fpc = (N - S) / (S * (N - 1)) rows.append({"S": S, "N": N, "iterate_variance": var, "finite_pop_correction_(N-S)/(S(N-1))": round(fpc, 6), "ratio_var_over_fpc": round(var / fpc, 6) if fpc > 0 else None}) print(" S=%-3d Var(theta)=%.4e (N-S)/(S(N-1))=%.5f ratio=%s" % (S, var, fpc, rows[-1]["ratio_var_over_fpc"]), flush=True) part = [r for r in rows if r["S"] < N] x = np.log([r["finite_pop_correction_(N-S)/(S(N-1))"] for r in part]) y = np.log([r["iterate_variance"] for r in part]) sl, ic = np.polyfit(x, y, 1) r2 = 1 - np.var(y - (sl * x + ic)) / np.var(y) ratios = [r["ratio_var_over_fpc"] for r in part] RESULTS["claim2"] = { "rows": rows, "seeds_per_cell": 24, "rounds": R, "loglog_slope_vs_finite_pop_correction": round(float(sl), 4), "predicted_slope": 1.0, "r2": round(float(r2), 4), "proportionality_const_spread": round(max(ratios) / min(ratios), 3), "variance_at_S_equals_N": rows[-1]["iterate_variance"], "vanishes_at_full_participation": bool(rows[-1]["iterate_variance"] < 1e-20)} print(" fit slope %.3f (predicted 1.0), R2 %.3f ; const spread %.2fx ; Var(S=N)=%.2e" % (sl, r2, RESULTS["claim2"]["proportionality_const_spread"], rows[-1]["iterate_variance"]), flush=True) def dec_consensus(cl, Wm, R=200, E=5, lr=0.5): N = len(cl); TH = np.zeros((N, D)) for _ in range(R): for i in range(N): for _ in range(E): TH[i] -= lr * grad(TH[i], cl[i][0], cl[i][1]) TH = Wm @ TH return float(np.mean(np.linalg.norm(TH - TH.mean(0), axis=1))) def claim5(): N = 8 cl, _ = make_clients(N, 0.8, seed=5) ring = np.zeros((N, N), int) for i in range(N): ring[i, (i + 1) % N] = ring[(i + 1) % N, i] = 1 W0, _ = metropolis(ring) rows = [] for a in (1.0, 0.8, 0.6, 0.45, 0.3, 0.2, 0.12): Wm = (1 - a) * np.eye(N) + a * W0 rho = float(np.sort(np.abs(np.linalg.eigvals(Wm)))[::-1][1]) err = dec_consensus(cl, Wm, R=250) rows.append({"lazy_alpha": a, "rho": round(rho, 5), "one_over_1_minus_rho": round(1 / (1 - rho), 3), "steady_state_consensus_error": err}) print(" alpha=%.2f rho=%.5f 1/(1-rho)=%7.2f consensus err=%.5e" % (a, rho, rows[-1]["one_over_1_minus_rho"], err), flush=True) x = np.log([r["one_over_1_minus_rho"] for r in rows]) y = np.log([r["steady_state_consensus_error"] for r in rows]) sl, ic = np.polyfit(x, y, 1) r2 = 1 - np.var(y - (sl * x + ic)) / np.var(y) RESULTS["claim5"] = { "graph": "8-node ring, fixed client assignment, lazy mixing sweeps rho", "rows": rows, "rounds": 250, "loglog_slope_vs_1_over_1_minus_rho": round(float(sl), 4), "r2": round(float(r2), 4)} print(" consensus error vs 1/(1-rho): slope %.3f, R2 %.4f" % (sl, r2), flush=True) if __name__ == "__main__": claim2(); claim5() json.dump(RESULTS, open("dpo_results2.json", "w"), indent=1)