import numpy as np, torch from collections import defaultdict def standardise(M): M=np.asarray(M,dtype=np.float64); mask=~np.eye(len(M),dtype=bool); v=M[mask] out=(M-v.mean())/v.std(); np.fill_diagonal(out,0.0); return out d=torch.load('/home/yurenh2/emm/artifacts/synth_v1/omit_size.pt',map_location='cpu') V0=d['visual_field'].double().numpy(); T0=d['text_field'].double().numpy() T=standardise(T0); V=standardise(V0); N=len(T) # exact automorphism-by-identical-rows of T (blind: uses T only) key={}; cls=defaultdict(list) Toff=T.copy(); np.fill_diagonal(Toff,0.0) # group scenes whose T rows agree after removing the two swapped coords groups=[]; used=np.zeros(N,bool) for i in range(N): if used[i]: continue g=[i]; used[i]=True for j in range(i+1,N): if used[j]: continue a=np.delete(Toff[i],[i,j]); b=np.delete(Toff[j],[i,j]) if np.abs(a-b).max()<1e-9 and abs(Toff[i,j]-max(Toff[i,i],0))<1e6: g.append(j); used[j]=True groups.append(g) sizes=np.array([len(g) for g in groups]) print("T exact-twin classes: total",len(groups),"; size histogram",np.bincount(sizes)[1:]) excess=int((sizes-1).sum()) print("scenes in non-trivial classes:",int(sizes[sizes>1].sum()),"; log|Aut| classes:",int((sizes>1).sum())) ceil=(N-int(sizes[sizes>1].sum())+int((sizes>1).sum()))/N print(f"blind accuracy ceiling if class is identified but member picked at random: {ceil:.3f}") # check the objective really is invariant: swap two members of a class import itertools p=np.arange(N) E0=(( (T*T).sum()+(V*V).sum() )-2*(T[np.ix_(p,p)]*V).sum())/(N*(N-1)) bad=0; tested=0 for g in groups: if len(g)>1: i,j=g[0],g[1]; q=p.copy(); q[[i,j]]=q[[j,i]] E1=(((T*T).sum()+(V*V).sum())-2*(T[np.ix_(q,q)]*V).sum())/(N*(N-1)) tested+=1 if abs(E1-E0)>1e-9: bad+=1 print(f"swapping twins changes E in {bad}/{tested} classes (0 means exact symmetry of the objective)") print(f"E(truth) = {E0:.10f}")