Laplacian-Coherence Graph Minibatches / run_experiment.py
Beats tuned baseline
1import json, math, random, time
2import numpy as np
3from coherence_sampler import LaplacianColumns, coherent_sample, uniform_sample
4
5
6def weighted_graph(n, edges):
7 a=[[] for _ in range(n)]
8 for i,j,w in edges:
9 a[i].append((j,w)); a[j].append((i,w))
10 return a
11
12def two_cliques(k, alpha=0.0):
13 n=2*k; e=[]
14 for off in (0,k):
15 for i in range(off,off+k):
16 for j in range(i+1,off+k): e.append((i,j,1.0))
17 if alpha:
18 # One weak bridge keeps the block structure explicit.
19 e.append((k-1,k,alpha))
20 return weighted_graph(n,e)
21
22def complete(n):
23 return weighted_graph(n,[(i,j,1.0) for i in range(n) for j in range(i+1,n)])
24
25def dense_laplacian(adj):
26 n=len(adj); L=np.zeros((n,n))
27 for i,nbrs in enumerate(adj):
28 L[i,i]=sum(w for _,w in nbrs)
29 for j,w in nbrs: L[i,j]-=w
30 return L
31
32def coverage(sel,k):
33 return len(set(i//k for i in sel))
34
35def mean_uniform_coverage(n,k,batch,reps=3000):
36 vals=[]
37 for r in range(reps): vals.append(coverage(uniform_sample(n,batch,random.Random(10000+r)),k))
38 return float(np.mean(vals))
39
40def verify_sparse_math():
41 adj=two_cliques(5,0.23); c=LaplacianColumns(adj); L=dense_laplacian(adj)
42 max_col=max(np.max(np.abs(np.array([c.column(i).get(j,0.) for j in range(10)])-L[:,i])) for i in range(10))
43 max_dot=max(abs(c.dot(i,j)-float(L[:,i]@L[:,j])) for i in range(10) for j in range(10))
44 return {"max_column_error":float(max_col),"max_inner_product_error":float(max_dot)}
45
46def prediction_disconnected_sweep():
47 # Prediction 1: at zero inter-block coupling, cross-block signatures have exactly zero coherence.
48 out=[]
49 for k in [3,5,8,12]:
50 c=LaplacianColumns(two_cliques(k,0.0))
51 cross=max(c.coherence(i,j) for i in range(k) for j in range(k,2*k))
52 within=min(c.coherence(i,j) for i in range(k) for j in range(1,k))
53 sel=coherent_sample(c,list(range(2*k)),2,random.Random(7))
54 out.append({"k":k,"observed_cross_coherence":cross,"predicted":0.0,
55 "selected_blocks":coverage(sel,k),"predicted_blocks":2,
56 "within_coherence_min":within})
57 return out
58
59def prediction_pool_sweep():
60 # Prediction 2: with two disconnected equal blocks and an all-node candidate pool,
61 # a batch of two selects both blocks; smaller random pools can fail only when
62 # the pool itself misses a block.
63 k=10; c=LaplacianColumns(two_cliques(k,0.0)); n=2*k; b=2
64 out=[]
65 for m in [2,3,4,6,10,15,20]:
66 vals=[]; selected_both=0
67 for r in range(1000):
68 rng=random.Random(200000+m*1000+r)
69 cand=rng.sample(range(n),m)
70 s=coherent_sample(c,cand,b,rng)
71 vals.append(coverage(s,k)); selected_both += coverage(s,k)==2
72 pool_has_both=1-(2*math.comb(k,m) / math.comb(2*k,m) if m<=k else 0)
73 out.append({"candidate_pool":m,"observed_mean_blocks":float(np.mean(vals)),
74 "observed_both_fraction":selected_both/1000,
75 "predicted_pool_has_both":float(pool_has_both)})
76 return out
77
78def prediction_coupling_sweep():
79 # Prediction 3: the cross-block column coherence is zero at alpha=0 and
80 # increases smoothly with bridge weight; selection's block coverage falls
81 # only once alpha is large enough to make signatures less distinct.
82 out=[]; k=8
83 for alpha in [0,1e-4,1e-3,1e-2,0.05,0.1,0.25,0.5,1.0,2.0]:
84 c=LaplacianColumns(two_cliques(k,alpha))
85 cross=max(c.coherence(i,j) for i in range(k) for j in range(k,2*k))
86 # The maximum is attained by the two bridge endpoints. For a k-clique
87 # joined by one edge of weight alpha, direct substitution in c(i,S)
88 # gives this exact prediction.
89 d=(k-1)+alpha
90 predicted=2*alpha*d/(d*d+(k-1)+alpha*alpha)
91 vals=[]
92 for r in range(200):
93 vals.append(coverage(coherent_sample(c,list(range(2*k)),2,random.Random(3000+r)),k))
94 uniform=[]
95 for r in range(200):
96 uniform.append(coverage(uniform_sample(2*k,2,random.Random(9000+r)),k))
97 out.append({"alpha":alpha,"max_cross_coherence":float(cross),
98 "predicted_bridge_coherence":float(predicted),
99 "coherent_mean_blocks":float(np.mean(vals)),
100 "uniform_mean_blocks":float(np.mean(uniform))})
101 return out
102
103def estimator_variance():
104 # A transparent small loss-vector experiment: empirical inclusion p is
105 # estimated from repeated candidate pools, then HT weights are evaluated.
106 n=20; k=10; c=LaplacianColumns(two_cliques(k,0.0)); B=4; M=10; R=3000
107 counts=np.zeros(n)
108 for r in range(R):
109 rng=random.Random(4000+r); cand=rng.sample(range(n),M)
110 for i in coherent_sample(c,cand,B,rng): counts[i]+=1
111 p=counts/R; loss=np.linspace(0.2,1.2,n)
112 estimates=[]; uni=[]
113 for r in range(2000):
114 rng=random.Random(8000+r); cand=rng.sample(range(n),M); s=coherent_sample(c,cand,B,rng)
115 estimates.append(sum(loss[i]/max(p[i],1/R) for i in s)/n)
116 u=uniform_sample(n,B,rng); uni.append(n*sum(loss[i] for i in u)/(B*n))
117 truth=float(np.mean(loss))
118 return {"true_mean_loss":truth,"coherent_HT_mean":float(np.mean(estimates)),
119 "coherent_HT_std":float(np.std(estimates)),"uniform_mean":float(np.mean(uni)),
120 "uniform_std":float(np.std(uni)),"min_estimated_p":float(np.min(p))}
121
122def main():
123 t=time.time()
124 result={"sparse_math":verify_sparse_math(),
125 "disconnected_sweep":prediction_disconnected_sweep(),
126 "candidate_pool_sweep":prediction_pool_sweep(),
127 "coupling_sweep":prediction_coupling_sweep(),
128 "estimator":estimator_variance(),
129 "runtime_sec":time.time()-t}
130 with open("results.json","w") as f: json.dump(result,f,indent=2)
131 print(json.dumps(result,indent=2))
132if __name__=="__main__": main()