File size: 19,705 Bytes
fa82a9d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/env python3
"""Audit Objects temporal confirmation without treating abstention as absence."""
from __future__ import annotations

import argparse
from collections import Counter, defaultdict
import hashlib
import itertools
import json
from pathlib import Path
import sys

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'src'))
from vbench_prompts_compile import annotation_jobs as J, records as R
from vbench_prompts_compile.experiments import cluster_summary, text_key
from vbench_prompts_compile.objects_repair import video_scores
from score_matrix import score_one, paired_rows, native_entity_codec
from cache_matrix_transforms import evidence_id
from analyze_object_controls import matched_contrasts
from render_matrix_tables import table, ci_text


def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def indexed(rows, key):
    result = {}
    for row in rows:
        identity = row[key]
        if identity in result:
            raise ValueError('duplicate ' + key + ': ' + str(identity))
        result[identity] = row
    return result


def regress(previous_path, current_path):
    unchanged, before, after = Counter(), [], []
    total = 0
    with Path(previous_path).open() as old, Path(current_path).open() as new:
        for a, b in itertools.zip_longest(old, new):
            if a is None or b is None:
                raise ValueError('changed matrix denominator')
            a, b = json.loads(a), json.loads(b)
            if (a['sample_id'], a['scheme']) != (b['sample_id'], b['scheme']):
                raise ValueError('changed observation identity/order')
            total += 1
            if a['task'] != 'objects' or a['scheme'] == 'Origin':
                if a != b:
                    raise ValueError('untargeted record changed: ' + a['sample_id'])
                unchanged[a['task'] + '/' + a['scheme']] += 1
            if a['task'] == 'objects':
                before.append(a)
                after.append(b)
    return {'total_rows': total, 'untouched_rows_exact': dict(unchanged)}, before, after


def frame_truth(target, other):
    """Three-state conjunction of independently stored visibility judgments."""
    if target not in {'visible', 'not_visible', 'uncertain'} or other not in {'visible', 'not_visible', 'uncertain'}:
        raise ValueError('invalid visibility label')
    if target == other == 'visible':
        return 'both_visible'
    if 'not_visible' in (target, other):
        return 'at_least_one_not_visible'
    return 'uncertain'


def legacy_result(entities, evidence):
    frames = evidence.get('frame_labels', [])
    states = ['missing' if i >= len(frames) or frames[i] is None else
              'supported' if all(name in frames[i] for name in entities) else 'absent' for i in range(16)]
    scores = [float(v == 'supported') for v in states]
    return {'score': sum(scores)/16, 'coverage': 1-states.count('missing')/16,
            'abstention': 0., 'frame_scores': scores,
            'objects_resolution': {'frame_states': states}}


def summarize_metrics(rows, keys):
    return {key: cluster_summary(rows, key) for key in keys}


def main(argv=None):
    p = argparse.ArgumentParser(description=__doc__)
    for name in ('matrix', 'cache', 'transforms', 'visibility', 'predictions', 'metadata',
                 'previous-paired', 'paired', 'score-report', 'out'):
        p.add_argument('--' + name, required=True)
    args = p.parse_args(argv)
    regression, before, after = regress(args.previous_paired, args.paired)
    matrix = indexed([r for r in J.read_jsonl(Path(args.matrix)) if r['task'] == 'objects'], 'sample_id')
    caches = indexed(J.read_jsonl(Path(args.cache)), 'relative_path')
    transforms = indexed(J.read_jsonl(Path(args.transforms)), 'evidence_id')
    reviews = indexed(J.read_jsonl(Path(args.visibility)), 'evidence_id')
    predictions = {text_key('objects', r['prompt']): r['target'] for r in
                   indexed(J.read_jsonl(Path(args.predictions)), 'request_id').values()}
    codec = native_entity_codec(json.loads(Path(args.metadata).read_text()))
    report = json.loads(Path(args.score_report).read_text())
    if not report.get('execution_complete') or report.get('objects_backend') != 'repair-v2':
        raise ValueError('complete Objects v2 execution required')
    # This audit must be attached to exactly the inputs/code of the scored run.
    for path, digest in report['input_sha256'].items():
        if sha(path) != digest:
            raise ValueError('score input changed: ' + path)
    for path, digest in report['scorer_source_sha256'].items():
        if sha(ROOT / path) != digest:
            raise ValueError('scorer source changed: ' + path)
    for key, review in reviews.items():
        digest = hashlib.sha256(json.dumps(transforms.get(key), sort_keys=True).encode()).hexdigest()
        if digest != review.get('transform_record_sha256'):
            raise ValueError('visibility review does not match transformed evidence')
    expected_ids = set(matrix)
    old_by_scheme = {s: indexed([r for r in before if r['scheme'] == s], 'sample_id')
                     for s in ('Origin', 'Repair-rule', 'Repair-model')}
    new_by_scheme = {s: indexed([r for r in after if r['scheme'] == s], 'sample_id') for s in old_by_scheme}
    if any(set(rows) != expected_ids for rows in [*old_by_scheme.values(), *new_by_scheme.values()]):
        raise ValueError('Objects matrix not fully paired')
    legacy_rows = paired_rows(list(matrix.values()), {'objects': caches}, transforms,
                             predictions, None, codec, reviews, objects_backend='legacy-official')
    if legacy_rows != before:
        raise ValueError('full legacy Objects rows no longer reproduce previous run')
    regression['legacy_full_rows_exact'] = len(legacy_rows)
    del legacy_rows
    checked = Counter()
    for sample_id, row in matrix.items():
        evidence = caches[row['relative_path']] if row['evidence_kind'] == 'original' else transforms[evidence_id(row)]
        for scheme in ('Repair-rule', 'Repair-model'):
            old, new = old_by_scheme[scheme][sample_id], new_by_scheme[scheme][sample_id]
            replay = score_one(row, scheme, evidence, predictions, None, codec, objects_backend='legacy-official')
            for field, dest in [('score', 'transformed_score'), ('coverage', 'transformed_coverage'),
                                ('abstention', 'transformed_abstention'), ('frame_scores', 'transformed_frame_scores'),
                                ('target', 'transformed_target'), ('backend_target', 'transformed_backend_target')]:
                if replay.get(field) != old[dest]:
                    raise ValueError('legacy Objects replay mismatch: ' + sample_id)
            if old['original_backend_target'] != new['original_backend_target'] or old['transformed_backend_target'] != new['transformed_backend_target']:
                raise ValueError('object requirement changed')
            if any(a > b for a, b in zip(new['transformed_frame_scores'], old['transformed_frame_scores'])):
                raise ValueError('temporal confirmation invented a positive')
            if old['original_coverage'] != new['original_coverage'] or old['transformed_coverage'] != new['transformed_coverage']:
                raise ValueError('changed backend evidence coverage')
            checked['legacy_replayed_rows'] += 1
            if row['transform'] == 'identity' or row.get('level') == 0:
                if new['original_score'] != new['transformed_score'] or new['original_objects_resolution'] != new['transformed_objects_resolution']:
                    raise ValueError('zero/identity is not exact')
                checked['identity_or_zero_exact_rows'] += 1
        rule, model = new_by_scheme['Repair-rule'][sample_id], new_by_scheme['Repair-model'][sample_id]
        for field in ('original_score', 'transformed_score', 'original_abstention', 'transformed_abstention',
                      'original_backend_target', 'transformed_backend_target', 'original_objects_resolution', 'transformed_objects_resolution'):
            if rule[field] != model[field]:
                raise ValueError('rule/model differ; separate ablation runs required')
    regression.update(checked)
    variants = ('legacy', 'adjacent_labels', 'adjacent_boxes')
    scored = {v: {} for v in variants}
    for sample_id, row in matrix.items():
        evidence = caches[row['relative_path']] if row['evidence_kind'] == 'original' else transforms[evidence_id(row)]
        entities = new_by_scheme['Repair-model'][sample_id]['transformed_backend_target']['entities']
        for variant in variants:
            value = legacy_result(entities, evidence) if variant == 'legacy' else video_scores(
                entities, evidence.get('frame_detections', []), confirmation=variant.removeprefix('adjacent_'))
            scored[variant][sample_id] = value
        actual = new_by_scheme['Repair-model'][sample_id]
        if scored['adjacent_boxes'][sample_id]['objects_resolution'] != actual['transformed_objects_resolution']:
            raise ValueError('live scorer audit replay mismatch: ' + sample_id)
    originals = {r['relative_path']: r for r in matrix.values() if r['transform'] == 'identity'}
    endpoints = [r for r in matrix.values() if r.get('occlusion_control') == 'target' and r.get('level') == 1]
    if len(originals) != len(caches) or len(endpoints) != len(caches) or len(reviews) != len(caches):
        raise ValueError('incomplete source or endpoint denominator')
    impact, endpoint_stats, visibility_stats, residuals = [], [], [], []
    for variant in variants:
        base_values, retained_frames = [], []
        for row in originals.values():
            a, b = scored['legacy'][row['sample_id']], scored[variant][row['sample_id']]
            base_values.append({'family': row['family'], 'score': b['score'], 'delta_from_legacy': b['score']-a['score'],
                'abstention': b['abstention'], 'positive_video_retained': b['score'] > 0, 'legacy_positive_video': a['score'] > 0})
            retained_frames.extend({'family': row['family'], 'retained': new > 0}
                for old, new in zip(a['frame_scores'], b['frame_scores']) if old > 0)
        impact.append({'variant': variant, **summarize_metrics(base_values, ('score', 'delta_from_legacy', 'abstention')),
            'original_positive_frame_retention': cluster_summary(retained_frames, 'retained'),
            'original_positive_video_retention': cluster_summary([r for r in base_values if r['legacy_positive_video']], 'positive_video_retained')})
        invisible, full_plan, isolated = [], [], []
        visibility_rows = defaultdict(list)
        for row in endpoints:
            key = evidence_id(row)
            review = reviews[key]
            a = scored[variant][originals[row['relative_path']]['sample_id']]
            b = scored[variant][row['sample_id']]
            states = b['objects_resolution']['frame_states']
            old_endpoint = scored['legacy'][row['sample_id']]
            item = {'family': row['family'], 'positive_video': b['score'] > 0,
                    'no_positive': b['coverage'] == 1 and b['score'] == 0,
                    'all_decisive_negative': all(s == 'absent' for s in states),
                    'positive_frame_fraction': b['score'], 'unconfirmed_frame_fraction': b['abstention'],
                    'positive_video_delta': int(b['score'] > 0)-int(old_endpoint['score'] > 0),
                    'positive_frame_fraction_delta': b['score']-old_endpoint['score']}
            known = bool(review.get('endpoint_invisible_verified'))
            full_plan.append({'family': row['family'], 'verified_no_positive': known and item['no_positive'],
                              'verified_all_decisive_negative': known and item['all_decisive_negative']})
            if known:
                invisible.append(item)
                if variant == 'adjacent_boxes' and (b['score'] > 0 or b['abstention'] > 0):
                    residuals.append({'relative_path': row['relative_path'], 'family': row['family'],
                        'legacy': scored['legacy'][row['sample_id']]['score'], 'repair': b['score'],
                        'abstention': b['abstention'], 'frame_states': states})
            old_base = scored['legacy'][originals[row['relative_path']]['sample_id']]
            if review.get('removal_verified') and old_base['score'] > 0:
                isolated.append({'family': row['family'], 'drop': a['score']-b['score'] >= .03,
                    'decisive_drop': a['score']-b['score'] >= .03 and a['abstention'] == b['abstention'] == 0,
                    'baseline_retained': a['score'] > 0})
            labels = review.get('labels')
            if labels:
                for side, value in [('before', a), ('after', b)]:
                    truths = [frame_truth(x, y) for x, y in zip(labels[side+'_target'], labels[side+'_other'])]
                    if len(truths) != 16:
                        raise ValueError('visibility frame slots changed')
                    for truth, state in zip(truths, value['objects_resolution']['frame_states']):
                        visibility_rows[side, truth].append({'family': row['family'], 'positive': state == 'supported',
                            'decisive_negative': state == 'absent', 'unconfirmed': state == 'unconfirmed', 'missing': state == 'missing'})
        endpoint_stats.append({'variant': variant, 'remaining_positive_videos': sum(r['positive_video'] for r in invisible),
            'no_positive_videos': sum(r['no_positive'] for r in invisible),
            'all_decisive_negative_videos': sum(r['all_decisive_negative'] for r in invisible),
            'invisible': summarize_metrics(invisible, ('positive_video', 'no_positive', 'all_decisive_negative', 'positive_frame_fraction', 'unconfirmed_frame_fraction', 'positive_video_delta', 'positive_frame_fraction_delta')),
            'full_plan': summarize_metrics(full_plan, ('verified_no_positive', 'verified_all_decisive_negative')),
            'same_legacy_positive_isolated_cohort': summarize_metrics(isolated, ('drop', 'decisive_drop', 'baseline_retained'))})
        visibility_stats.extend({'variant': variant, 'side': key[0], 'truth': key[1],
            **summarize_metrics(rows, ('positive', 'decisive_negative', 'unconfirmed', 'missing'))}
            for key, rows in sorted(visibility_rows.items()))
    controls = [{'scheme': scheme, 'level': level,
        **summarize_metrics(rows, ('original', 'target', 'background', 'target_minus_background'))}
        for (scheme, level), rows in sorted(matched_contrasts(after).items())]
    out = Path(args.out)
    out.mkdir(parents=True, exist_ok=True)
    R.write_jsonl(out/'residual-endpoints.jsonl', residuals)
    summary = {'version': 'objects-repair-v2', 'regression': regression, 'original_impact': impact,
        'endpoints': endpoint_stats, 'visibility': visibility_stats, 'matched_controls': controls,
        'planned_videos': len(caches), 'planned_conditions': len(matrix),
        'visibility_status': dict(Counter(r['status'] for r in reviews.values())),
        'summary': [r for r in report['summary'] if r['task'] == 'objects'],
        'input_sha256': {v: sha(v) for k, v in vars(args).items() if k != 'out'},
        'source_sha256': {n: sha(ROOT/n) for n in ['scripts/evaluate_objects_repair.py', 'scripts/score_matrix.py',
            'scripts/render_matrix_tables.py', 'scripts/analyze_object_controls.py',
            'src/vbench_prompts_compile/objects_repair.py', 'src/vbench_prompts_compile/experiments.py']},
        'protocol_sha256': sha(ROOT/'docs/plans/15-objects-temporal-repair.md'),
        'notes': ['Post-hoc engineering repair, not an independent blind evaluation.',
            'Same 0.5 IoU rule on originals and every control; no threshold sweep or new training/inference.',
            'Visibility model consensus is used for evaluation only, never by the scorer; not human gold.',
            'Unconfirmed predictions are abstentions scored zero, not verified absence; report both no-positive and decisive-negative endpoints.',
            'Original positive frame retention measures agreement with the old detector, not recall; visibility-stratified rates are separate.',
            'The same legacy-positive isolated-removal cohort is retained for comparing variants.',
            '2000 source-family bootstrap draws, seed 20260919; frames are not independent observations.',
            'Sustained detector hallucinations and brief/fast-moving true objects remain limitations.']}
    rows = [{'Variant': r['variant'], 'Videos': r['score']['n'], 'Mean score (CI)': ci_text(r['score']),
             'Delta from legacy (CI)': ci_text(r['delta_from_legacy']), 'Abstain (CI)': ci_text(r['abstention']),
             'Old positive frames retained (CI)': ci_text(r['original_positive_frame_retention']),
             'Old positive videos retained (CI)': ci_text(r['original_positive_video_retention'])} for r in impact]
    table(out, 'original-impact', rows, list(rows[0]))
    rows = [{'Variant': r['variant'], 'Invisible videos': r['invisible']['positive_video']['n'],
             'Still positive': r['remaining_positive_videos'], 'No positive': r['no_positive_videos'],
             'All decisive negative': r['all_decisive_negative_videos'],
             'Positive video delta (CI)': ci_text(r['invisible']['positive_video_delta']),
             'Positive frames (CI)': ci_text(r['invisible']['positive_frame_fraction']),
             'Unconfirmed frames (CI)': ci_text(r['invisible']['unconfirmed_frame_fraction']),
             'Same isolated cohort drop (CI)': ci_text(r['same_legacy_positive_isolated_cohort']['drop']),
             'Same isolated cohort decisive drop (CI)': ci_text(r['same_legacy_positive_isolated_cohort']['decisive_drop'])}
            for r in endpoint_stats]
    table(out, 'endpoints', rows, list(rows[0]))
    rows = [{'Variant': r['variant'], 'Side': r['side'], 'Model visibility': r['truth'],
             'Frames': r['positive']['n'], 'Families': r['positive']['families'],
             'Positive rate (CI)': ci_text(r['positive']), 'Decisive negative rate (CI)': ci_text(r['decisive_negative']),
             'Unconfirmed rate (CI)': ci_text(r['unconfirmed'])} for r in visibility_stats]
    table(out, 'visibility', rows, list(rows[0]))
    rows = [{'Scheme': r['scheme'], 'Level': r['level'], 'Complete videos': r['target']['n'],
             'Target (CI)': ci_text(r['target']), 'Background (CI)': ci_text(r['background']),
             'Target minus background (CI)': ci_text(r['target_minus_background'])} for r in controls]
    table(out, 'controls', rows, list(rows[0]))
    rows = [{'Scheme': r['scheme'], 'Transform': r['transform'], 'N': r['eligible'],
             'Original': r['original_score']['estimate'], 'Transformed': r['transformed_score']['estimate'],
             'Delta (CI)': ci_text(r['paired_delta']), 'Coverage': r['transformed_coverage']['estimate'],
             'Abstain': r['transformed_abstention']['estimate']} for r in summary['summary']]
    table(out, 'scores', rows, list(rows[0]))
    summary['output_sha256'] = {p.name: sha(p) for p in sorted(out.iterdir()) if p.suffix in {'.md', '.csv', '.tex', '.jsonl'}}
    J.atomic_json(out/'report.json', summary)
    print(json.dumps({'regression': regression, 'remaining_positive_endpoints':
        {r['variant']: r['remaining_positive_videos'] for r in endpoint_stats}, 'out': str(out)}, indent=2))
    return 0


if __name__ == '__main__':
    raise SystemExit(main())