From ef104fe4f07713bf11f266d2954b7446f176f8ae Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sat, 1 Aug 2026 19:32:58 -0500 Subject: Record the matching battery: fifteen solvers, one amplifier Adds MATCHING_RESULTS.md. Entropic GW annealed and PATH both reach 0.961 on the reference field, above the 0.958 the project's own pipeline reached after months, and neither had been run. GW reaches 0.881 on the rank-8 field where GRAMPA reaches 0.076 -- which retires the morning's rank-ladder conclusion, since that ladder was run entirely with GRAMPA and GRAMPA degrades on clustered eigenvalues. The hard instance yields to amplification rather than a better solver: descent multiplies a partial answer by about four, so the job is to feed it a start that is 10-20% correct rather than to replace it. Co-Authored-By: Claude --- logs/verify.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 logs/verify.py (limited to 'logs/verify.py') diff --git a/logs/verify.py b/logs/verify.py new file mode 100644 index 0000000..17fd643 --- /dev/null +++ b/logs/verify.py @@ -0,0 +1,59 @@ +import sys, numpy as np, ot, torch +sys.path.insert(0,'/home/yurenh2/emm') +from scipy.optimize import linear_sum_assignment +from worldalign.synth_fast_gate import ClosedFormEnergy, fast_pair_descent +from worldalign.synth_triangle_gate import standardized +DEV=torch.device("cuda:1") +def standardise(m): + mask=~np.eye(len(m),dtype=bool); v=m[mask] + o=(m-v.mean())/v.std(); np.fill_diagonal(o,0.0); return o +def tlb(V,S): + n=len(V); a=np.sort(V,1); b=np.sort(S,1) + return (a*a).sum(1)[:,None]/n+(b*b).sum(1)[None,:]/n-2*(a@b.T)/n +st=torch.load("/home/yurenh2/emm/artifacts/synth_v1/omit_size.pt",map_location='cpu',weights_only=False) +V=standardise(st["visual_field"].double().numpy()); T=standardise(st["text_field"].double().numpy()) +n=len(V); w=ot.unif(n) +rng=np.random.default_rng(0); hid=rng.permutation(n); S=T[np.ix_(hid,hid)] +Vg=standardized(torch.from_numpy(V).to(DEV)).double(); Sg=standardized(torch.from_numpy(S).to(DEV)).double() +en=ClosedFormEnergy(Sg,Vg,1.0,0.0,64) +M=tlb(V,S); M=M/M.max() +G=np.outer(w,w) +for a in (0.0,0.2): + G=ot.gromov.fused_gromov_wasserstein(M,V,S,w,w,"square_loss",alpha=a,G0=G,max_iter=200,tol_rel=1e-9) +_,cols=linear_sum_assignment(-G) +fin=fast_pair_descent(Sg,Vg,torch.from_numpy(np.ascontiguousarray(cols)).to(DEV),2000).cpu().numpy() +truth=np.argsort(hid) +Ef=float(en.energy(torch.from_numpy(np.ascontiguousarray(fin)).to(DEV)[None])[0]) +Et=float(en.energy(torch.from_numpy(np.ascontiguousarray(truth)).to(DEV)[None])[0]) +acc=float((hid[fin]==np.arange(n)).mean()) +print("accuracy=%.4f E_found=%.10f E_truth=%.10f diff=%.3e"%(acc,Ef,Et,Ef-Et)) +wrong=np.where(hid[fin]!=np.arange(n))[0] +print("n_wrong=%d"%len(wrong)) +# for each wrong scene i: correlation between S-row of assigned node and S-row of true node +Sal=S # in shuffled index space +corr=[] +vcorr=[] +for i in wrong: + a_idx=fin[i]; t_idx=truth[i] + x=np.delete(Sal[a_idx],[a_idx,t_idx]); y=np.delete(Sal[t_idx],[a_idx,t_idx]) + corr.append(np.corrcoef(x,y)[0,1]) + u=np.delete(V[i],[i]); + # visual side: is the visually-assigned scene similar to i? compare V rows of i and of the scene truly at a_idx + j=hid[a_idx] + v1=np.delete(V[i],[i,j]); v2=np.delete(V[j],[i,j]) + vcorr.append(np.corrcoef(v1,v2)[0,1]) +rng2=np.random.default_rng(7); pairs=rng2.integers(0,n,(len(wrong),2)) +base=[np.corrcoef(np.delete(Sal[p],[p,q]),np.delete(Sal[q],[p,q]))[0,1] for p,q in pairs if p!=q] +print("text-row corr of confused partners: median=%.3f mean=%.3f | random pair baseline median=%.3f"%(np.median(corr),np.mean(corr),np.median(base))) +print("visual-row corr of confused partners: median=%.3f"%np.median(vcorr)) +# cycle structure of the error +perm=hid[fin] +seen=set(); cyc=[] +for i in range(n): + if i in seen or perm[i]==i: continue + c=0; j=i + while j not in seen: + seen.add(j); j=perm[j]; c+=1 + cyc.append(c) +import collections +print("error cycle lengths:", dict(collections.Counter(cyc))) -- cgit v1.2.3