"""Cached-evidence representative ablation; serial cost projections, not wall time. Selection uses appearance-only groups; unavailable matcher evidence is masked. """ from pathlib import Path import json import sys import statistics import time from collections import Counter ROOT=Path(__file__).resolve().parents[3];sys.path.insert(0,str(ROOT/'code')) from identity_lanes import DecisionPolicy from analyze import decide,metrics,propagate,infer_groups DIR=Path(__file__).resolve().parent;OUT=DIR/'evidence' def main(): observations=json.loads((OUT/'observations.json').read_text());analysis=json.loads((OUT/'analysis.json').read_text());family=json.loads((DIR/'group-truth.json').read_text())['families'];summary={} for res,d in observations['resolutions'].items(): rows=d['observations'];lookup={x['id']:x for x in rows}; groups=analysis['resolutions'][res]['appearance_groups']; sig=observations['signatures'] baseline={o['id']:decide(o,'coverage-first',sig) for o in rows} arms={} for arm in ('sharpest-one','sharpest-two','all'): selected=set() for group in groups: ordered=sorted(group,key=lambda i:lookup[i]['sharpness'],reverse=True) selected.update(ordered[:len(group) if arm=='all' else 2 if arm=='sharpest-two' else 1]) # Unlike the OCR-representative arm, unselected members have no # matcher observation. Their individually read OCR may still speak. masked=[] for o in rows: clone=dict(o) if o['id'] not in selected:clone['rank']=[];clone['local']={} masked.append(clone) individual={o['id']:decide(o,'coverage-first',sig) for o in masked} available_groups=infer_groups(d,analysis['resolutions'][res]['guarded_edges'],{o['id']:decide(o,'strict',sig) for o in masked}) inferred,changes=propagate(masked,available_groups,individual,sig,guarded=True) matcher_ms=sum(lookup[i]['local_ms'] for i in selected) descriptor_ms=sum(lookup[i]['pair_features_ms'] for i in lookup) # group prep retains descriptor extraction pair_ms=sum(p['pair_ms'] for p in d['pairs']) arms[arm]={'matcher_calls':len(selected),'matcher_ms_serial_projection':round(matcher_ms,1),'group_descriptor_ms':round(descriptor_ms,1),'pair_match_ms':round(pair_ms,1),'total_identity_and_group_ms_serial_projection':round(matcher_ms+descriptor_ms+pair_ms,1),'individual':metrics(masked,individual,family,baseline),'inferred':metrics(masked,inferred,family,baseline),'changes':changes} summary[res]=arms for arm,v in arms.items():print(res,arm,v['matcher_calls'],v['inferred']['correct'],v['inferred']['wrong'],v['total_identity_and_group_ms_serial_projection']) (OUT/'representative.json').write_text(json.dumps({'provenance':observations['provenance'],'arms':summary},ensure_ascii=False,indent=2)) if __name__=='__main__':main()