"""Original finite counted joint-distribution MI/CMI, all log2 units bits."""
from collections import Counter
from itertools import product,permutations
from fractions import Fraction as F
import math,json

def valid(joint):
 if type(joint) is not dict or not joint or len(joint)>16 or any(type(k) is not tuple or len(k)!=3 or any(type(v) not in [int,str] or (type(v) is str and len(v)>12) for v in k) or type(n) is not int or n<0 for k,n in joint.items()) or not 1<=sum(joint.values())<=100:raise ValueError('nonempty bounded three-variable joint integer counts, nonnegative, positive total')
 return {k:n for k,n in joint.items() if n}

def project(joint,positions):
 out=Counter()
 for row,n in joint.items():out[tuple(row[i] for i in positions)]+=n
 return out

def entropy(counts):
 n=sum(counts.values());return -sum(float(F(c,n))*math.log2(float(F(c,n))) for c in counts.values() if c)

def mi(joint,xpos,ypos):
 # Marginals are derived from one joint table, never independent supplied totals.
 n=sum(joint.values());xy=project(joint,xpos+ypos);xs=project(joint,xpos);ys=project(joint,ypos);out=0.0;terms=[]
 for key,c in xy.items():
  x=key[:len(xpos)];y=key[len(xpos):];p=F(c,n);ratio=F(c*n,xs[x]*ys[y]);value=float(p)*math.log2(float(ratio));out+=value;terms.append({'x':x,'y':y,'p':str(p),'ratio_to_product_marginals':str(ratio),'term_bits':value})
 alternative=entropy(xs)+entropy(ys)-entropy(xy);assert abs(out-alternative)<1e-12
 return out,terms

def analyze(joint,conditioning_values):
 d=valid(joint);n=sum(d.values());ixy,terms=mi(d,[0],[1]);ixzy,_=mi(d,[0,2],[1]);izy,_=mi(d,[2],[1]);conditional=0.0;slices=[]
 for z in conditioning_values:
  part={k:c for k,c in d.items() if k[2]==z};mass=F(sum(part.values()),n)
  if not part:slices.append({'z':z,'mass':'0','status':'zero_mass_skipped_no_conditional_distribution'});continue
  value,_=mi(part,[0],[1]);conditional+=float(mass)*value;slices.append({'z':z,'mass':str(mass),'conditional_MI_bits':value})
 assert set(k[2] for k in d)<=set(conditioning_values)
 entropy_oracle=entropy(project(d,[0,2]))+entropy(project(d,[1,2]))-entropy(project(d,[2]))-entropy(d);chain=ixzy-izy
 pz=project(d,[2]);pxz=project(d,[0,2]);pyz=project(d,[1,2]);direct=sum(float(F(c,n))*math.log2(float(F(c*pz[(z,)],pxz[(x,z)]*pyz[(y,z)]))) for (x,y,z),c in d.items())
 assert abs(conditional-entropy_oracle)<1e-12 and abs(conditional-chain)<1e-12 and abs(conditional-direct)<1e-12
 return {'joint':[{'X':x,'Y':y,'Z':z,'count':c,'probability':str(F(c,n))} for (x,y,z),c in sorted(d.items())],'MI_X_Y_bits':ixy,'MI_XZ_Y_bits':ixzy,'MI_Z_Y_bits':izy,'MI_terms':terms,'conditional_slices':slices,'CMI_X_Y_given_Z_bits':conditional,'independent_entropy_CMI_bits':entropy_oracle,'independent_chain_rule_CMI_bits':chain,'independent_joint_ratio_CMI_bits':direct}

def checked_marginal_totals(joint_counts,xcounts,ycounts):
 # A diagnostic rejects independently supplied inconsistent totals before ratios.
 if sum(joint_counts.values())!=sum(xcounts.values()) or sum(joint_counts.values())!=sum(ycounts.values()):raise ValueError('mismatched marginal totals')

def demo():
 xor={(x,x^z,z):1 for x,z in product([0,1],repeat=2)};main=analyze(xor,[0,1,2]);assert abs(main['MI_X_Y_bits'])<1e-12 and abs(main['CMI_X_Y_given_Z_bits']-1)<1e-12 and abs(main['MI_XZ_Y_bits']-1)<1e-12
 independent={(x,y,z):1 for x,y,z in product([0,1],repeat=3)};ind=analyze(independent,[0,1]);assert abs(ind['CMI_X_Y_given_Z_bits'])<1e-12
 curved={(x,x*x,0):1 for x in [-1,0,1]};second=analyze(curved,[0]);cov=F(sum(x*y*c for (x,y,z),c in curved.items()),3)-F(sum(x*c for (x,y,z),c in curved.items()),3)*F(sum(y*c for (x,y,z),c in curved.items()),3);assert cov==0 and second['MI_X_Y_bits']>0
 # Explicit tied ranks of y=1,0,1 are5/2,1,5/2; rank covariance also zero.
 xr=[F(1),F(2),F(3)];yr=[F(5,2),F(1),F(5,2)];rankcov=sum((x-2)*(y-2) for x,y in zip(xr,yr))/3;assert rankcov==0
 perms=0
 for xx,yy,zz in product(list(permutations([0,1])),repeat=3):
  changed={(xx[x],yy[y],zz[z]):n for (x,y,z),n in xor.items()};out=analyze(changed,[0,1]);assert abs(out['MI_X_Y_bits'])<1e-12 and abs(out['CMI_X_Y_given_Z_bits']-1)<1e-12;perms+=1
 rejected=[]
 for label,bad in [('negative',{(0,0,0):-1}),('bool_count',{(0,0,0):True}),('fractional_count',{(0,0,0):1.5}),('empty',{}),('zero_total',{(0,0,0):0}),('bad_shape',{(0,0):1})]:
  try:valid(bad)
  except ValueError:rejected.append(label)
  else:raise AssertionError('expected rejection')
 try:checked_marginal_totals({('a','b'):2},{'a':1},{'b':2})
 except ValueError:rejected.append('mismatched_supplied_marginal_totals')
 else:raise AssertionError('expected rejection')
 return {'equiprobable_XOR':main,'independent_three_bits':ind,'nonlinear_X_squared_zero_correlations':{'analysis':second,'ordinary_covariance':str(cov),'tied_rank_covariance':str(rankcov),'nonzero_variances_so_correlations_zero':True},'all_eight_binary_category_relabelings':perms,'invalid_cases_rejected':rejected,'units':'log2 bits; sklearn mutual_info_score uses natural-log nats and is not executed','scope':'complete authored finite counted joint law, no population estimation/pvalue/causality/feature-ranking advice; zero-mass conditional values skipped explicitly; Cornell Remark11.1 item2 first displayed line has an apparent typo and is not copied, independent entropy identity uses H(X,Y|Z)'}
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=True,indent=2))
