"""Only self-authored finite toys/material QA. No model/corpus.
Toy EER: deterministic tie blocks + rate-space interpolation.
Runtime existing tmp/lop01-runtime NumPy2.2.6/Matplotlib3.10.7.
"""
from pathlib import Path
import json,hashlib,re,math,itertools,importlib.util,sys
from urllib.parse import unquote
from statistics import NormalDist
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
sys.dont_write_bytecode=True
ROOT=Path(__file__).resolve().parents[1]
checks=[];values={}
def check(name,a,e,tol=1e-10):
 ok=bool(np.allclose(a,e,rtol=tol,atol=tol));checks.append(dict(name=name,passed=ok,actual=np.asarray(a).tolist(),expected=np.asarray(e).tolist(),tolerance=tol))
 if not ok:raise AssertionError((name,a,e))
def rates(b,s,t):return float(np.mean(np.asarray(b)<t)),float(np.mean(np.asarray(s)>=t))
def curve(b,s):
 b=np.asarray(b);s=np.asarray(s)
 if not b.size or not s.size:raise ValueError('missing class')
 if not np.all(np.isfinite(np.r_[b,s])):raise ValueError('nonfinite')
 ts=np.r_[-np.inf,np.unique(np.r_[b,s]),np.inf];pairs=np.array([rates(b,s,t) for t in ts]);ix=np.r_[True,np.any(np.diff(pairs,axis=0)!=0,axis=1)]
 return ts[ix],pairs[ix,0],pairs[ix,1]
def eer(b,s):
 _,m,f=curve(b,s);d=m-f;eq=np.flatnonzero(d==0)
 if eq.size:return float(m[eq[0]])
 j=np.flatnonzero(d>0)[0];i=j-1;w=-d[i]/(d[j]-d[i]);return float(m[i]+w*(m[j]-m[i]))
def auc(b,s):return float(np.mean([1 if x>y else .5 if x==y else 0 for x in b for y in s]))
def pr(b,s):
 rec=[0.];prec=[1.]
 for t in sorted(set(b+s),reverse=True):
  tp=sum(x>=t for x in b);fp=sum(x>=t for x in s);rec.append(tp/len(b));prec.append(tp/(tp+fp))
 return np.asarray(rec),np.asarray(prec)
def mincost(b,s):
 _,m,f=curve(b,s);return float(np.min(1.9*m+f))
def cllr(b,s):return float((np.logaddexp(0,-np.asarray(b)).mean()+np.logaddexp(0,np.asarray(s)).mean())/(2*np.log(2)))
b=[.9,.6,.4];s=[.8,.4,.1];t,m,f=curve(b,s);r,p=pr(b,s)
check('six-trial sweep',np.c_[m,f],[[0,1],[0,2/3],[1/3,1/3],[2/3,1/3],[2/3,0],[1,0]])
check('AUC pair',auc(b,s),13/18);check('ROC area independent',-np.trapezoid(1-m,f),auc(b,s))
check('AP step',np.sum(np.diff(r)*p[1:]),34/45);check('PR trapezoid',np.trapezoid(p,r),133/180)
check('zero-FA fixed-rule upper bound',[1-.05**(1/100),1-.05**(1/10000)],[.029513049607039932,.00029952835977664627])
check('EER exact',eer(b,s),1/3);check('EER interpolation',eer([.9,.5],[.8,.7,.1]),.5)
check('nearest averages',[(.5+2/3)/2,(.5+1/3)/2],[7/12,5/12]);check('minDCF',mincost(b,s),2/3)
check('threshold equality',rates([.8,.4],[.6,.1],.6),[.5,.5]);check('sign reversal',auc([-x for x in b],[-x for x in s]),1-13/18)
check('increasing transform',auc(np.exp(b),np.exp(s)),13/18);check('all-tie EER',eer([1,1],[1,1]),.5);check('all-tie AUC',auc([1,1],[1,1]),.5)
check('prevalence',[.5*.9/(.5*.9+.5*.1),.01*.9/(.01*.9+.99*.1)],[.9,1/12]);check('accuracy risk',[.99*.99+.01*.9,.99*.01+100*.01*.1],[.9891,.1099])
tau=-np.log(1.9);check('Bayes threshold',tau,-.6418538861723947)
check('same-ranking actual cost',[1.9*rates([2,1],[-1,-2],tau)[0]+rates([2,1],[-1,-2],tau)[1],1.9*rates([12,11],[9,8],tau)[0]+rates([12,11],[9,8],tau)[1]],[0,1])
check('shift threshold restores',rates([12,11],[9,8],tau+10),[0,0]);check('shift mincost',[mincost([2,1],[-1,-2]),mincost([12,11],[9,8])],[0,0])
check('zeroLLR Cllr',cllr([0,0],[0,0]),1);check('softplus stable',np.logaddexp(0,[-1000,0,1000]),[0,np.log(2),1000])
values['cllr_original']=cllr([2,1],[-1,-2]);values['cllr_shifted']=cllr([12,11],[9,8])
check('aDCF',[.8*.5+.1*.5,.45/.8],[.45,.5625]);C0=.8*.1+.1*.2;C1=.8*.9-.1*.2;C2=.1*10*.6
check('tandem rates',[.05+.95*.1,.95*.2,.1*.6],[.145,.19,.06]);check('tandem decomposition',[C0,C1,C2,C0+C1*.05+C2*.1,.8*.145+.1*.19+.06],[.1,.7,.6,.195,.195])
check('modern tDCF normalization',.195/(.1+min(.7,.6)),.2785714285714286)
check('duration local rates',[1/8,1/2],[.125,.5])
check('local interval',[1/2,1/2,1/3,8/10],[.5,.5,1/3,.8]);check('offset pooled',[eer([3,4,103,104],[1,2,101,102]),auc([3,4,103,104],[1,2,101,102])],[.5,.75])
check('group weights',[.5*.4,.5*.1+.5*.2,.1*.4,.9*.1+.1*.2],[.2,.15,.04,.11]);check('search optimism',np.mean([min(a,b) for a,b in itertools.product([8,12],repeat=2)]),9)
check('SD tCI',[np.std([6,8,10],ddof=1),8-4.302652729911275*2/np.sqrt(3),8+4.302652729911275*2/np.sqrt(3)],[2,3.031724576560908,12.968275423439092])
groups=[([2],[1],[2],[3]),([1],[2],[2],[1])];deltas=[];pooled=[]
for draw in itertools.product(range(2),repeat=2):
 a=np.mean([eer(groups[g][0],groups[g][1]) for g in draw]);z=np.mean([eer(groups[g][2],groups[g][3]) for g in draw]);deltas.append(a-z)
 cols=[sum([groups[g][k] for g in draw],[]) for k in range(4)];pooled.append(eer(cols[0],cols[1])-eer(cols[2],cols[3]))
check('paired macro draws',deltas,[-1,0,0,1]);check('paired pooled coincidence',pooled,[-1,0,0,1]);check('missing-class probability',2*.5**4,1/8)
check('McNemar enumerate',2*sum(math.comb(8,k) for k in [0,1])/2**8,18/256);check('20tests false-positive',1-.95**20,.6415140775914581)
check('Holm stops',[.01<=.05/3,.03<=.05/2],[True,False]);comb=list(itertools.product([-1,1],repeat=2))
check('probe reliance toy',[sum((a>=0)==(a==1) for a,c in comb)/4,sum((a+2*c>=0)==(a==1) for a,c in comb)/4],[1,.5])
check('ID join',[auc([3,1],[2,0]),auc([2,3],[0,1])],[.75,1]);check('RTF latency',[.12/4,1/.12,2.56+.12],[.03,25/3,2.68])
cache=13*128*768*4;check('cache bytes',[cache,cache/2**20],[5111808,4.875]);values['cache_total_decimal_GB']=cache*20980/1e9
check('exposures',[100*20,300*20,2000/300],[2000,6000,20/3]);pts=np.array([[8,.1],[7,.2],[9,.15],[7,.1]])
frontier=[i for i,x in enumerate(pts) if not any(np.all(y<=x) and np.any(y<x) for y in pts)];check('finite frontier',frontier,[3])
ref=ROOT/'research/reference-calculate_modules.py';spec=importlib.util.spec_from_file_location('asv5_reference',ref);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module)
check('official tie scorer',module.compute_eer(np.ones(2),np.ones(2))[0],1);check('official actDCF',module.compute_actDCF(np.array([12,11]),np.array([9,8]),.05,1,10)[0],1)
check('official finite Cllr',module.calculate_CLLR(np.array([2,1]),np.array([-1,-2])),values['cllr_original']);values['official_source_sha256']=hashlib.sha256(ref.read_bytes()).hexdigest()
assets=ROOT/'assets';assets.mkdir(exist_ok=True);plt.rcParams.update({'font.size':10,'figure.dpi':150})
fig,axs=plt.subplots(1,3,figsize=(13,4),layout='constrained')
axs[0].plot(f,1-m,'o-');axs[0].plot([0,1],[0,1],'--',color='grey');axs[0].set(xlabel='FPR (spoof accepted)',ylabel='TPR (B accepted)',title='B-positive ROC: AUC = 13/18',xlim=(-.03,1.03),ylim=(-.03,1.03))
q=NormalDist().inv_cdf;axs[1].plot([q(x) for x in np.clip(f,.01,.99)],[q(x) for x in np.clip(m,.01,.99)],'o-');axs[1].set(xlabel='Normal quantile of FA',ylabel='Normal quantile of miss',title='DET: drawing clipped at 1% / 99%')
axs[2].plot(r,p,'o-',label='Trapezoid = 133/180');axs[2].step(r,p,where='pre',label='AP = 34/45');axs[2].set(xlabel='Recall B',ylabel='Precision B',title='PR: same finite scores',xlim=(-.03,1.03),ylim=(0,1.05));axs[2].legend(fontsize=8)
for ax in axs:ax.grid(alpha=.25)
fig.suptitle('Self-authored simulated six-trial metric example');fig.savefig(assets/'metric-toy.png');plt.close(fig)
fig,axs=plt.subplots(1,2,figsize=(10,4),layout='constrained')
axs[0].bar(['Original','Shift +10'],[values['cllr_original'],values['cllr_shifted']],color=['#1f77b4','#d66a20']);axs[0].set(ylabel='Cllr (LLR interpretation)',title='Same rank: EER = 0, minDCF = 0',ylim=(0,7.4));axs[0].text(0,.7,'actDCF = 0',ha='center');axs[0].text(1,6.7,'actDCF = 1',ha='center')
axs[1].bar([-1,0,1],[.25,.5,.25],width=.5,color='#36856e');axs[1].set(xlabel='Delta macro EER: A - B',ylabel='Empirical draw probability',title='Two-group paired bootstrap: 4 draws',ylim=(0,.6));axs[1].set_xticks([-1,0,1]);fig.suptitle('Self-authored toys: no project model results');fig.savefig(assets/'cost-bootstrap-toy.png');plt.close(fig)
issues=[];link_count=0;mds=sorted(ROOT.rglob('*.md'))+[ROOT.parent/'00-BAT-DAU.md',ROOT.parent/'05-DANH-GIA-VA-THUC-NGHIEM.md']
for md in mds:
 txt=md.read_text(encoding='utf8')
 if '\ufffd' in txt:issues.append(f'encoding replacement: {md}')
 if txt.count('<details>')!=txt.count('</details>'):issues.append(f'details mismatch: {md}')
 if txt.count('$$')%2:issues.append(f'math mismatch: {md}')
 fence=None;cols=None
 for ln,line in enumerate(txt.splitlines(),1):
  match=re.match('^\\s*('+chr(96)+'{3,}|~{3,})',line)
  if match:
   char=match.group(1)[0]
   if fence is None:fence=char
   elif fence==char:fence=None
   continue
  if fence:continue
  if line.startswith('|'):
   n=len(re.split(r'(?<!\\)\|',line))-2
   if cols is None:cols=n
   elif cols!=n:issues.append(f'table columns {md.name}:{ln}')
  else:cols=None
 if fence:issues.append(f'unclosed fence {md}')
 for target in re.findall(r'\]\(([^\n]+?)\)',txt):
  target=target.strip().strip('<>')
  if re.match(r'^(https?://|mailto:|codex:)',target):continue
  target=unquote(target.split('#')[0])
  if not target:continue
  target=re.sub(r':\d+$','',target);resolved=Path(target) if re.match(r'^[A-Za-z]:[/\\]',target) else md.parent/target;link_count+=1
  if not resolved.exists():issues.append(f'broken local link {md.name}: {target}')
baseline=json.loads((ROOT/'research/baseline-hashes.json').read_text(encoding='utf8'));allowed={str(ROOT.parent/'00-BAT-DAU.md').replace('\\','/'),str(ROOT.parent/'05-DANH-GIA-VA-THUC-NGHIEM.md').replace('\\','/')};changes=[]
for p,sha in baseline.items():
 if not Path(p).exists():issues.append(f'protected missing {p}');continue
 now=hashlib.sha256(Path(p).read_bytes()).hexdigest()
 if now!=sha:
  changes.append(p)
  if p.replace('\\','/') not in allowed:issues.append(f'protected changed {p}')
report=dict(date='2026-10-11',scope='toy arithmetic/material QA, not model verification',runtime=dict(python=sys.version,numpy=np.__version__,matplotlib=matplotlib.__version__),checks=checks,values=values,markdown_files=len(mds),local_links=link_count,baseline_files=len(baseline),allowed_changes=changes,issues=issues,passed=not issues and all(c['passed'] for c in checks))
(ROOT/'research/validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8')
print(json.dumps({k:report[k] for k in ['passed','markdown_files','local_links','baseline_files','allowed_changes','issues','values']},ensure_ascii=False,indent=2));print('Numerical checks:',len(checks))
if issues:sys.exit(1)
