"""Authored ranked-ID evaluation: positive grade >=1, linear gain, no score ties."""
import math,json,itertools
def evaluate(grades,ranking,k):
    if not isinstance(grades,dict) or not grades:raise ValueError('nonempty judged universe')
    if any(not isinstance(g,int) or isinstance(g,bool) or not 0<=g<=3 for g in grades.values()):raise ValueError('reviewed integer grades 0..3 required')
    if not isinstance(ranking,list) or len(ranking)!=len(grades) or len(set(ranking))!=len(ranking) or set(ranking)!=set(grades):raise ValueError('unique complete ranked IDs')
    if not isinstance(k,int) or isinstance(k,bool) or not 1<=k<=len(ranking):raise ValueError('k must be 1..N')
    values=[grades[x] for x in ranking[:k]]
    rr=next((1/i for i,g in enumerate(values,1) if g>=1),0.0)
    dcg=sum(g/math.log2(i+1) for i,g in enumerate(values,1))
    ideal=sum(g/math.log2(i+1) for i,g in enumerate(sorted(grades.values(),reverse=True)[:k],1))
    return {'rr_at_k':rr,'dcg_linear_gain':dcg,'ideal_dcg':ideal,'ndcg_at_k':dcg/ideal if ideal else 0.0,'zero_ideal_policy':'zero'}
def demo():
    g={'A':3,'B':2,'C':1,'D':0}
    q1=evaluate(g,['D','A','B','C'],3);q2=evaluate(g,['A','C','B','D'],3)
    q3=evaluate(dict.fromkeys(g,0),['A','B','C','D'],3);best=evaluate(g,['A','B','C','D'],3)
    assert q1['rr_at_k']==.5 and q2['rr_at_k']==best['rr_at_k']==1 and q3['rr_at_k']==0
    assert q2['ndcg_at_k']<best['ndcg_at_k']==1
    count=0
    for order in itertools.permutations(g):
        for k in range(1,5):
            result=evaluate(g,list(order),k);assert 0<=result['ndcg_at_k']<=1;count+=1
    rejected=0
    for grades,ranking,k in [(g,['A','A','C','D'],3),(g,['A','B','C','X'],3),(g,['A','B','C'],3),({'A':None},['A'],1),({'A':-1},['A'],1),(g,list(g),0),(g,list(g),True)]:
        try:evaluate(grades,ranking,k)
        except ValueError:rejected+=1
        else:raise AssertionError('invalid contract accepted')
    return {'fixture':'authored four documents; no search service queried','judged_grades':g,'queries_at_3':{'Q1':q1,'Q2':q2,'Q3':q3},'mrr_at_3_macro':sum(x['rr_at_k'] for x in [q1,q2,q3])/3,'Q2_ideal_order':best,'permutation_cutoff_checks':count,'invalid_contracts_rejected':rejected}
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=False,indent=2))
