"""Perplexity arithmetic on supplied synthetic target probabilities."""
import math,json
def nll_count(probabilities,mask):
    if len(probabilities)!=len(mask):raise ValueError("length mismatch")
    nll=0.;count=0
    for p,m in zip(probabilities,mask):
        if not isinstance(m,bool):raise ValueError("boolean score mask required")
        if not m:continue
        if not isinstance(p,(int,float)) or isinstance(p,bool) or not math.isfinite(p) or not 0<p<=1:raise ValueError("scored probability must be in (0,1]")
        nll-=math.log(p);count+=1
    if not count:raise ValueError("no scored target")
    return nll,count
if __name__=="__main__":
    a=nll_count([None,.5],[False,True]);b=nll_count([None,.25,.25,.25],[False,True,True,True])
    parts=[math.exp(nll/n) for nll,n in [a,b]]
    pooled=math.exp((a[0]+b[0])/(a[1]+b[1]))
    assert parts==[2.,4.] and math.isclose(pooled,2**(7/4),abs_tol=1e-14)
    assert sum(parts)/2==3 and (parts[0]+3*parts[1])/4==3.5
    rejected=0
    for probs,mask in [([0],[True]),([1.1],[True]),([None],[True]),([None],[False])]:
        try:nll_count(probs,mask)
        except ValueError:rejected+=1
    assert rejected==4
    print(json.dumps({"scored_targets":[a[1],b[1]],"NLL_sums":[a[0],b[0]],"individual_PPL":parts,"pooled_PPL":pooled,"plain_mean_PPL":3,"length_weighted_mean_PPL":3.5,"invalid_cases_rejected":rejected},indent=2))
