Files
Jyotisha/docs/research/artifacts/rectification-width-funnel-2026-10-16/funnel.py
T

68 lines
4.4 KiB
Python

"""Why does the delivered interval stay wide? Per-question funnel on v5 (research harness, static probe list)."""
import json, sys, statistics as st
from pathlib import Path
WT = Path('/workspace/Jyotisha/.worktrees/karaka-interval-20261016')
sys.path.insert(0, str(WT)); sys.path.insert(0, str(WT/'scripts'))
from scripts.research import cluster_width_probe as cwp
from scripts.research import probe_supply_after_six as ps
from scripts.research import cluster_width_lib as cl
cases = json.loads((WT/'references/real_case_calibration/minute_rectification_holdout_v5.json').read_text())['cases']
shard, n = map(int, sys.argv[1].split('/'))
MAXQ = 12
out = []
def clock(t): h, m = map(int, t.split(':')); return h*60+m
for i, case in enumerate(cases):
if i % n != shard: continue
true_time = str(case['birth']['time'])[:5]
for R in (10, 30, 60):
req = cwp.scoring_request_for(case, R)
built = cwp.build_event_contribution_matrix(req)
rows = cwp.score_from_matrix(req, built)
contexts = list(built.get('static_contexts') or [])
window_times = [str(r['time'])[:5] for r in rows]
raw = cwp.raw_signature_clusters(contexts)
probes = cwp.discriminating_event_probes({**req, 'refresh_probes': False}, built, scan=cwp.window_scan(built),
candidate_times=list(window_times), representative_time=true_time, today=cwp.TODAY)
merged, _ = cwp.merge_adjacent_traced(raw, cwp._by_time_rows(rows), max_clusters=cwp.Variant('baseline').max_clusters,
max_peak_delta=cwp.Variant('baseline').max_peak_delta)
public = cwp.public_from_clusters(merged, rows)
reps = [str(r['time'])[:5] for r in public]
size = {str(r['time'])[:5]: len(r.get('cluster_times') or [1]) for r in public}
truth_rep = next((str(r['time'])[:5] for r in public if true_time in (r.get('cluster_times') or [str(r['time'])[:5]])), None)
prior = {t: float(r.get('score') or 0) for t, r in zip(reps, public)}
scores = dict(prior); conflicts = {t: 0 for t in reps}; elim = set()
prior_spread = max(prior.values()) - min(prior.values())
prior_truth_gap = max(prior.values()) - prior.get(truth_rep, 0)
steps = []
for q, probe in enumerate(probes[:MAXQ]):
alive = [t for t in reps if t not in elim]
yes, no = ps.outcome_groups(probe)
ans = ps.optimal_answer(probe, true_time)
sides = {}
for t in alive:
d = 'support' if t in yes else 'conflict' if t in no else ps.inherit_direction(t, yes, no)
sides[t] = d
truth_d = 'support' if ans == 'yes' else 'conflict' if ans == 'no' else 'neutral'
same = sum(size[t] for t in alive if sides[t] == truth_d) ; opp = sum(size[t] for t in alive if sides[t] not in (truth_d, 'neutral'))
neu = sum(size[t] for t in alive if sides[t] == 'neutral')
if ans is not None:
scores, conflicts, elim = ps.apply_answer(scores, conflicts, elim, probe, ans, reps)
valid = cl.still_valid_public(public, scores, elim, lead=8)
d = cl.delivery_from_public(valid)
steps.append(dict(q=q+1, answer=ans, same=same, opp=opp, neutral=neu, alive_min=sum(size[t] for t in reps if t not in elim),
span8=d['width'], set8=len(d['times']), domain=probe.get('domain') or probe.get('kind')))
def deliver(lead, upto):
sc = dict(prior); cf = {t: 0 for t in reps}; el = set()
for probe in probes[:upto]:
a = ps.optimal_answer(probe, true_time)
if a: sc, cf, el = ps.apply_answer(sc, cf, el, probe, a, reps)
v = cl.still_valid_public(public, sc, el, lead=lead); d = cl.delivery_from_public(v)
alive = [t for t in reps if t not in el]
best = max(sc[t] for t in alive); leaders = [t for t in alive if sc[t] == best]
return dict(span=d['width'], set=len(d['times']), cov=true_time in d['times'], top1=leaders == [truth_rep])
out.append(dict(case=case['case_id'], R=R, true=true_time, n_reps=len(reps), n_probes=len(probes),
prior_spread=prior_spread, prior_truth_gap=prior_truth_gap, steps=steps,
alt={f'L{lead}_Q{q}': deliver(lead, q) for lead in (8, 4, 2) for q in (6, 12)}))
print(case['case_id'], flush=True)
Path(f'funnel_shard{shard}.json').write_text(json.dumps(out)); print('DONE')