"""A-D/F: paired policies, geometry-only rows, label-blind inference, oracle membership. No production edits. All accuracy counters use the frozen observation file. """ from pathlib import Path import sys import json import time import itertools import math from collections import Counter import numpy as np ROOT=Path(__file__).resolve().parents[3] sys.path.insert(0,str(ROOT/'code')) from identity_lanes import DecisionPolicy DIR=Path(__file__).resolve().parent POLICIES={'strict':4.,'balanced':2.,'coverage-first':.5,'near-argmax':0.} def decide(o, policy, signatures, use_text=True): p=DecisionPolicy(local_accept=POLICIES[policy],signatures=signatures) text=o['text'] if use_text else {} read=o['read'] if use_text else '' d=p.decide(o['rank'],text,text_read=read) answer=d['best_sku'] # Near-argmax retains actual geometric candidates even when the confidence # margin is negative; no arbitrary catalog row when there are no features. if policy=='near-argmax' and not answer and o['rank']: candidate=o['rank'][0][0] tr=sorted(text.items(),key=lambda x:x[1],reverse=True) if not p._contradicts_size(read,candidate) and not (tr and tr[0][0]!=candidate and tr[0][1]>text.get(candidate,0)): answer=candidate return answer def pair_value(p, name): s=p['signals'] if name=='combined': return .35*s['hue']+.35*s['pixels']+.30*min(1,s['inliers']/10) return s[name] def geometric_edge(p): s=p['signals'] return -.25<=s['gap']<=.75 and s['height']>=.65 and s['bottom']<=.25 def infer_groups(data, edge_yes, anchors): """Contiguous appearance-supported runs; strong contradiction is a hard cut.""" edges={(p['a'],p['b']):edge_yes[i] and geometric_edge(p) for i,p in enumerate(data['pairs'])} groups=[] for row in data['rows']: group=[]; support=None for item in row: anchor=anchors.get(item) if group and (not edges.get((group[-1],item),False) or (anchor and support and anchor!=support)): groups.append(group); group=[]; support=None group.append(item) if anchor: support=anchor if group: groups.append(group) return groups def strength(o, answer): if not answer: return 0. if o['rank'] and o['rank'][0][0]==answer: return max(0,o['rank'][0][1])+2*o['text'].get(answer,0) return 2*o['text'].get(answer,0) def propagate(observations, groups, answers, signatures, guarded=False): """One strongest independently supported prototype, not majority voting. Group evidence can rescue weak members, never overwrite strict contradiction. """ lookup={x['id']:x for x in observations} result=dict(answers); rescued=[] strict={i:decide(o,'strict',signatures) for i,o in lookup.items()} guard=DecisionPolicy(signatures=signatures) for group in groups: seeds=[i for i in group if strict[i]] if not seeds: continue seed=max(seeds,key=lambda i:strength(lookup[i],strict[i])) sku=strict[seed] if guarded: # Two distinct facings corroborate a seed, or one unusually strong # geometric observation does. Repeated reads of one crop do not. corroborated=sum(strict[i]==sku for i in seeds)>=2 strong_visual=bool(lookup[seed]['rank'] and lookup[seed]['rank'][0][0]==sku and lookup[seed]['rank'][0][1]>=8) if not (corroborated or strong_visual): continue # Size contradictions belong to the entire repeated-artwork run. # Abstain from propagation rather than spread a lone OCR mistake. if any(guard._contradicts_size(lookup[i]['read'],sku) for i in group): continue for i in group: o=lookup[i] if strict[i] and strict[i]!=sku: continue if guard._contradicts_size(o['read'],sku): continue # Independently decisive text also splits a weak visual member. text_sku=guard.decide([],o['text'],text_read=o['read'])['best_sku'] if text_sku and text_sku!=sku: continue if result[i]!=sku: rescued.append({'id':i,'seed':seed,'from':result[i],'to':sku}) result[i]=sku return result,rescued def metrics(observations, answers, families, before=None): total=len(observations); known=sum(o['truth']!='unknown' for o in observations) shown=correct=unknown=sibling=wrong_known=0 rescue=correction=damage=0 for o in observations: i=o['id']; a=answers.get(i); t=o['truth'] shown+=bool(a); correct+=bool(a and a==t) unknown+=bool(a and t=='unknown') wrong_known+=bool(a and t!='unknown' and a!=t) sibling+=bool(a and a!=t and t in families and families.get(a)==families[t]) if before is not None: rescue+=bool(not before.get(i) and a==t) correction+=bool(before.get(i) and before[i]!=t and a==t) damage+=bool(before.get(i)==t and a!=t) return {'facings':total,'catalog_facings':known,'shown':shown,'correct':correct,'wrong':shown-correct, 'visible_coverage':shown/total,'correct_coverage':correct/known,'precision':correct/shown if shown else None, 'false_label_rate':(shown-correct)/total,'unknown_false_accepts':unknown,'unknown_total':total-known, 'sibling_confusions':sibling,'wrong_catalog':wrong_known,'blank_ratio':1-shown/total, 'rescued_correct':rescue,'corrected_wrong':correction,'damaged_correct':damage} def confusion(actual,predicted): tp=sum(a and p for a,p in zip(actual,predicted)); fp=sum(not a and p for a,p in zip(actual,predicted)) tn=sum(not a and not p for a,p in zip(actual,predicted)); fn=sum(a and not p for a,p in zip(actual,predicted)) return {'tp':tp,'fp':fp,'tn':tn,'fn':fn,'precision':tp/(tp+fp) if tp+fp else None,'recall':tp/(tp+fn) if tp+fn else None,'boundary_accuracy':(tp+tn)/max(1,len(actual)),'boundary_recall':tn/(tn+fp) if tn+fp else None} def auc(pos,neg): return sum((a>b)+.5*(a==b) for a in pos for b in neg)/(len(pos)*len(neg)) if pos and neg else None def fit_threshold(pairs, name): # Weighted classification error, so long repeated runs cannot hide boundaries. candidates=sorted(set(pair_value(p,name) for p in pairs)) candidates=[-1e-9]+[(a+b)/2 for a,b in zip(candidates,candidates[1:])]+[max(candidates,default=0)+1e-9] def loss(t): yes=[p for p in pairs if p['same']]; no=[p for p in pairs if not p['same']] return sum(pair_value(p,name)=t for p in no)/max(1,len(no)) return min(candidates,key=lambda t:(loss(t),-t)) def chain_dp(data, edge_yes, observations, signatures): """Potts chain MAP by Viterbi, O(n*K^2), no framework or truth input.""" lookup={o['id']:o for o in observations}; skus=[None]+list(signatures) edge={(p['a'],p['b']):edge_yes[i] and geometric_edge(p) for i,p in enumerate(data['pairs'])} result={} for row in data['rows']: unary=[] for i in row: o=lookup[i]; values={s:e['inliers']*(.25+e['color']) for s,e in o['local'].items()} maximum=max(values.values(),default=1.) u=[.4]+[values.get(s,0)/maximum+.4*o['text'].get(s,0) for s in skus[1:]] anchor=decide(o,'strict',signatures) if anchor: u=[v if s==anchor else v-4 for s,v in zip(skus,u)] unary.append(np.asarray(u)) score=unary[0]; backs=[] for n in range(1,len(row)): reward=.6 if edge.get((row[n-1],row[n]),False) else 0. table=score[:,None]+np.eye(len(skus))*reward back=table.argmax(axis=0); score=unary[n]+table[back,np.arange(len(skus))]; backs.append(back) best=int(score.argmax()); labels=[best] for back in reversed(backs): best=int(back[best]); labels.append(best) for i,k in zip(row,reversed(labels)): result[i]=skus[k] return result def main(): evidence=json.loads((DIR/'evidence/observations.json').read_text()); annotation=json.loads((DIR/'group-truth.json').read_text()) families=annotation['families']; report={'provenance':evidence['provenance'],'resolutions':{}} for res,data in evidence['resolutions'].items(): observations=data['observations']; lookup={o['id']:o for o in observations} keys={o['id']:o['truth'] for o in observations if o['truth']!='unknown'} family=dict(families) for g in annotation['unknown_visual_groups']: keys.update({i:g['key'] for i in g['members']}); family[g['key']]=g['family'] scored=[] for p in data['pairs']: if p['a'] not in keys or p['b'] not in keys: continue a,b=keys[p['a']],keys[p['b']] scored.append({**p,'same':a==b,'class':'same' if a==b else 'sibling' if family[a]==family[b] else 'unrelated'}) signal_report={} names=['hue','pixels','logo_pixels','edge','inliers','match_fraction','layout_cover','aspect','height','ocr_jaccard','combined'] predictions={} for name in names: thresholds={r:fit_threshold([p for p in scored if p['row']!=r],name) for r in range(len(data['rows']))} predictions[name]=[pair_value(p,name)>=thresholds[p['row']] and geometric_edge(p) for p in data['pairs']] byclass={c:[pair_value(p,name) for p in scored if p['class']==c] for c in ['same','sibling','unrelated']} signal_report[name]={'thresholds_leave_row_out':thresholds,'same_vs_sibling_auc':auc(byclass['same'],byclass['sibling']),'same_vs_unrelated_auc':auc(byclass['same'],byclass['unrelated']), 'classes':{c:{'n':len(v),'p50':float(np.median(v)) if v else None} for c,v in byclass.items()},'held_row_confusion':confusion([p['same'] for p in scored],[pair_value(p,name)>=thresholds[p['row']] and geometric_edge(p) for p in scored])} # Preselected combined arm; do not pick the best held-row score after seeing truth. edge_yes=predictions['combined'] anchors={o['id']:decide(o,'strict',evidence['signatures']) for o in observations} start=time.perf_counter(); groups=infer_groups(data,edge_yes,anchors); group_ms=(time.perf_counter()-start)*1000 # One boundary variable: correspondence is required in addition to the # combined score. Stops color-only joins between different artworks. guarded_edges=[yes and p['signals']['inliers']>=4 for yes,p in zip(edge_yes,data['pairs'])] guarded_groups=infer_groups(data,guarded_edges,anchors) appearance_groups=infer_groups(data,guarded_edges,{}) oracle=[] for row in data['rows']: run=[]; key=None for i in row: nextkey=keys.get(i,('unscored',i)) if run and key!=nextkey: oracle.append(run); run=[] key=nextkey; run.append(i) if run:oracle.append(run) all_pairs=list(itertools.combinations(sorted(keys),2)) def memberships(gs): return {i:g for g,ids in enumerate(gs) for i in ids} membership=memberships(groups); oracle_membership=memberships(oracle) group_eval=confusion([oracle_membership[a]==oracle_membership[b] for a,b in all_pairs],[membership[a]==membership[b] for a,b in all_pairs]) guarded_membership=memberships(guarded_groups) guarded_eval=confusion([oracle_membership[a]==oracle_membership[b] for a,b in all_pairs],[guarded_membership[a]==guarded_membership[b] for a,b in all_pairs]) boundary_eval=confusion([p['same'] for p in scored],[guarded_membership[p['a']]==guarded_membership[p['b']] for p in scored]) results={}; answers_output={} for policy in POLICIES: base={o['id']:decide(o,policy,evidence['signatures']) for o in observations} results[policy]={}; answers_output[policy]={} for mode,gs in [('individual',[[o['id']] for o in observations]),('oracle',oracle),('inferred',groups),('seed_guard',groups),('boundary_guard',guarded_groups),('guarded',guarded_groups)]: start=time.perf_counter(); answers,changed=propagate(observations,gs,base,evidence['signatures'],guarded=mode in ('seed_guard','guarded')) if mode!='individual' else (base,[]); fusion_ms=(time.perf_counter()-start)*1000 results[policy][mode]={**metrics(observations,answers,families,base),'changes':changed,'fusion_ms':fusion_ms} answers_output[policy][mode]=answers start=time.perf_counter(); dp=chain_dp(data,edge_yes,observations,evidence['signatures']); dp_ms=(time.perf_counter()-start)*1000 results['dp']={**metrics(observations,dp,families,answers_output['coverage-first']['individual']),'inference_ms':dp_ms} # Utility sensitivity, static one-second exposure proxy; temporal loss is in replay. sensitivity={} for wrong_cost in [.5,1.,2.,4.,8.]: cells={f'{p}/{mode}':(m['facings']-m['shown']+wrong_cost*m['wrong'])/m['facings'] for p,arms in results.items() if p!='dp' for mode,m in arms.items()} sensitivity[str(wrong_cost)]={'loss':cells,'best':min(cells,key=cells.get)} deployable={k:v for k,v in cells.items() if not k.endswith('/oracle')} sensitivity[str(wrong_cost)]['best_nonoracle']=min(deployable,key=deployable.get) report['resolutions'][res]={'pair_counts':dict(Counter(p['class'] for p in scored)), 'signals':signal_report,'pairs':scored,'groups':groups,'oracle_groups':oracle,'group_ms':group_ms,'group_membership':group_eval,'policies':results,'answers':answers_output,'utility_sensitivity':sensitivity} report['resolutions'][res]['guarded_groups']=guarded_groups report['resolutions'][res]['appearance_groups']=appearance_groups report['resolutions'][res]['guarded_group_membership']=guarded_eval report['resolutions'][res]['guarded_adjacent_boundaries']=boundary_eval report['resolutions'][res]['guarded_edges']=guarded_edges print(res,'groups',len(groups),'pair counts',report['resolutions'][res]['pair_counts']) for p in POLICIES: print(p,{mode:(m['shown'],m['correct'],m['wrong'],m['rescued_correct'],m['damaged_correct']) for mode,m in results[p].items()}) (DIR/'evidence/analysis.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)) plot(report) def plot(report): # SVG uses stdlib only; both axes and denominators are explicit. colors={'strict':'#475569','balanced':'#2563eb','coverage-first':'#059669','near-argmax':'#dc2626'} parts=[''] for j,res in enumerate(['full','live']): x0=70+j*490; y0=405 parts.append(f'{res}: visible coverage / precision') for t in [0,.25,.5,.75,1]: parts.append(f'{t:.0%}{t:.0%}') for p in POLICIES: for mode in ['individual','guarded']: m=report['resolutions'][res]['policies'][p][mode]; x=x0+390*m['visible_coverage']; y=y0-350*(m['precision'] or 0) parts.append(f'{p}/{mode}: {m["shown"]}/109 shown, {m["correct"]}/64 correct, {m["wrong"]} false') parts.append(f'Any labels / 109; filled = guarded groups, hollow = individual') for n,(policy,color) in enumerate(colors.items()): parts.append(f'{policy}') parts.append('');(DIR/'evidence/frontier.svg').write_text(''.join(parts)) if __name__=='__main__': main()