"""Original exact categorical supports and base2 KL/JS, explicit Infinity tag."""
from fractions import Fraction as F
from itertools import permutations
import math,json

def distribution(rows):
 if type(rows) is not list or not 1<=len(rows)<=4 or any(type(r) is not tuple or len(r)!=2 or type(r[0]) is not str or not r[0] or type(r[1]) not in [int,F] or not 0<=r[1]<=1 or F(r[1]).denominator>1000000 for r in rows) or len({x[0] for x in rows})!=len(rows) or sum(x[1] for x in rows)!=1:raise ValueError('1..4 unique labeled exact normalized masses; each reduced denominator<=1000000')
 return {k:F(v) for k,v in rows}

def entropy(p):return -sum(float(v)*math.log2(float(v)) for v in p.values() if v)

def kl(p,q):
 if set(p)!=set(q):raise ValueError('same category universe')
 if any(p[k]>0 and q[k]==0 for k in p):return 'Infinity'
 return sum(float(v)*math.log2(float(v/q[k])) for k,v in p.items() if v)

def compare(prows,qrows):
 p=distribution(prows);q=distribution(qrows)
 if set(p)!=set(q):raise ValueError('same category universe')
 m={k:(p[k]+q[k])/2 for k in p};pq=kl(p,q);qp=kl(q,p);pm=kl(p,m);qm=kl(q,m);div=(pm+qm)/2;ent=entropy(m)-(entropy(p)+entropy(q))/2;assert abs(div-ent)<1e-12
 cross='Infinity' if pq=='Infinity' else -sum(float(v)*math.log2(float(q[k])) for k,v in p.items() if v)
 if pq!='Infinity':assert abs(cross-entropy(p)-pq)<1e-12
 assert div>=-1e-12
 return {'P':{k:str(v) for k,v in p.items()},'Q':{k:str(v) for k,v in q.items()},'midpoint':{k:str(v) for k,v in m.items()},'KL_P_Q_bits':pq,'KL_Q_P_bits':qp,'JS_divergence_bits':div,'JS_distance_sqrt_divergence':math.sqrt(max(0,div)),'independent_midpoint_entropy_JS_bits':ent,'entropy_P_bits':entropy(p),'crossentropy_P_Q_bits':cross,'zero_mass_terms_skipped_as_limit':True}

def demo():
 disjoint=compare([('a',1),('b',0)],[('a',0),('b',1)]);assert disjoint['KL_P_Q_bits']=='Infinity' and disjoint['JS_divergence_bits']==disjoint['JS_distance_sqrt_divergence']==1
 p=[('a',F(1,2)),('b',F(1,2))];q=[('a',F(3,4)),('b',F(1,4))];finite=compare(p,q);assert abs(finite['KL_P_Q_bits']-finite['KL_Q_P_bits'])>1e-3
 checks=0
 for pp,qq in [(p,q),(list(reversed(p)),q),(p,list(reversed(q))),(list(reversed(p)),list(reversed(q)))]:
  row=compare(pp,qq);assert abs(row['JS_divergence_bits']-finite['JS_divergence_bits'])<1e-12 and abs(row['KL_P_Q_bits']-finite['KL_P_Q_bits'])<1e-12;checks+=1
 labeled=compare([('a',F(1,4)),('b',F(3,4))],[('b',F(3,4)),('a',F(1,4))]);assert labeled['KL_P_Q_bits']==0
 naive_zip_wrong=compare([('0',F(1,4)),('1',F(3,4))],[('0',F(3,4)),('1',F(1,4))]);assert naive_zip_wrong['KL_P_Q_bits']>0
 rejected=[]
 for label,pp,qq in [('negative',[('a',-1),('b',2)],q),('nonfinite',[('a',float('inf'))],q),('empty',[],q),('zero_total',[('a',0)],q),('not_normalized',[('a',F(1,2))],q),('universe_mismatch',[('x',1)],[('a',1)]),('duplicate_labels',[('a',F(1,2)),('a',F(1,2))],q),('tiny_positive_underflow_outside_profile',[('a',F(1,10**400)),('b',1-F(1,10**400))],[('a',F(1,10**400)),('b',1-F(1,10**400))]),('denominator_just_above_limit',[('a',F(1,1000001)),('b',F(1000000,1000001))],[('a',1)])]:
  try:compare(pp,qq)
  except ValueError:rejected.append(label)
  else:raise AssertionError('expected rejection')
 boundary=compare([('a',F(1,1000000)),('b',F(999999,1000000))],[('a',F(1,1000000)),('b',F(999999,1000000))]);assert boundary['KL_P_Q_bits']==boundary['JS_divergence_bits']==0
 out={'denominator_limit':1000000,'minimum_mass_boundary_identical':boundary,'disjoint_support':disjoint,'finite_asymmetric_example':finite,'all_four_input_order_permutations':checks,'identical_labeled_law_in_different_order':labeled,'naive_position_alignment_wrong':naive_zip_wrong,'invalid_cases_rejected':rejected,'scope':'strict normalized labeled finite laws with reduced input denominators<=1000000, log2 bits, no hidden epsilon/renormalization; JSdistance is sqrt divergence; Infinity explicit string never invalid JSON; no SciPy/model quality/calibration/drift/significance or metric claim for KL'}
 json.dumps(out,allow_nan=False);return out
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=True,indent=2,allow_nan=False))
