Skip to content

Commit 65387e6

Browse files
committed
trace ace: add V107 support-conditioned prior
1 parent da668e0 commit 65387e6

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env python3
2+
"""V107: support-conditioned V74 prior on top of frozen V97.
3+
4+
Hypothesis from V106: V74 helps supported/seen-objective regimes but hurts exact-unseen
5+
objectives. Preserve V97 exactly on unseen objectives and add a small fixed V74 prior
6+
only when the exact objective has fold-local training support.
7+
8+
This is a fixed-law confirmation: no labels select weights.
9+
"""
10+
from __future__ import annotations
11+
import argparse, hashlib, json
12+
from pathlib import Path
13+
import numpy as np
14+
from sklearn.cluster import KMeans
15+
from sklearn.linear_model import LogisticRegression
16+
from sklearn.metrics import log_loss
17+
18+
from v71_mastery_events import load_transcript
19+
from v74_semantic_objective_prior import semantic_prior_predict
20+
from v75_canonical_trajectory import load_training, SEED
21+
from v85_evidence_state import build_v75
22+
from v93_shift_robust_validation import obj_family, style_matrix
23+
from v94_related_control import segmented_control, build_control
24+
25+
EPS=1e-5
26+
W_V97_UNSEEN=0.35
27+
W74_SEEN=0.20
28+
29+
30+
def ll(y,p): return float(log_loss(y,np.clip(p,EPS,1-EPS),labels=[0,1]))
31+
def fit_lr(X,y,tr,va):
32+
m=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X[tr],y[tr])
33+
return np.clip(m.predict_proba(X[va])[:,1],EPS,1-EPS)
34+
def bucket(x,n=5):
35+
h=int(hashlib.sha256(str(x).encode()).hexdigest()[:16],16)
36+
return h % n
37+
def hash_folds(groups,n=5):
38+
g=np.asarray(groups).astype(str); idx=np.arange(len(g)); out=[]
39+
gb=np.asarray([bucket(x,n) for x in g])
40+
for k in range(n):
41+
va=idx[gb==k]; tr=idx[gb!=k]; out.append((tr,va))
42+
return out
43+
def mixed_support_folds(obj,sess,n=5):
44+
idx=np.arange(len(obj)); ob=np.asarray([bucket(x,n) for x in obj]); sb=np.asarray([bucket(x,n) for x in sess]); out=[]
45+
for k in range(n):
46+
cold=ob==k
47+
seen_session=(ob!=k)&(sb==k)
48+
va=idx[cold|seen_session]; tr=idx[~(cold|seen_session)]; out.append((tr,va))
49+
return out
50+
def unseen_mask(keys,tr,va):
51+
seen=set(keys[tr].tolist()); return np.asarray([keys[i] not in seen for i in va],bool)
52+
53+
def run(a):
54+
f=load_training(a.features,a.labels).reset_index(drop=True)
55+
cache={sid:load_transcript(a.transcripts/f'{sid}.csv') for sid in f.session_id.astype(str).unique()}
56+
rt=[]; rz=[]
57+
for i,r in f.iterrows():
58+
t,z=segmented_control(cache[str(r.session_id)],str(r.learning_objective),'related'); rt.append(t); rz.append(z)
59+
if (i+1)%2500==0: print('rows',i+1,flush=True)
60+
X75=build_v75(f,cache); Xr=build_control(rt,rz); y=f.target.to_numpy(int)
61+
obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy()
62+
support=f.learning_objective.astype(str).to_numpy(); sess=f.session_id.astype(str).to_numpy()
63+
fam=f.learning_objective.astype(str).map(obj_family).astype(str).to_numpy()
64+
style=KMeans(n_clusters=5,random_state=137,n_init=10).fit(style_matrix(f,cache)).labels_.astype(str)
65+
worlds={
66+
'objective_cold':hash_folds(obj),
67+
'session_cold':hash_folds(sess),
68+
'objective_family_cold':hash_folds(fam),
69+
'style_cold':hash_folds(style),
70+
'mixed_support':mixed_support_folds(obj,sess),
71+
}
72+
out={'law':{'unseen':'V97 = .65 V75 + .35 RELATED','seen':'0.80 V75 + 0.20 V74'},'worlds':{}}
73+
gains=[]
74+
for name,sp in worlds.items():
75+
p97=np.zeros(len(y)); p107=np.zeros(len(y)); U=np.zeros(len(y),bool); folds=[]
76+
for k,(tr,va) in enumerate(sp,1):
77+
p75=fit_lr(X75,y,tr,va); pr=fit_lr(Xr,y,tr,va); p74,_=semantic_prior_predict(f.iloc[tr],f.iloc[va])
78+
uns=unseen_mask(support,tr,va); U[va]=uns
79+
q97=np.where(uns,.65*p75+.35*pr,p75)
80+
q107=np.where(uns,q97,.80*p75+.20*p74)
81+
q97=np.clip(q97,EPS,1-EPS); q107=np.clip(q107,EPS,1-EPS)
82+
p97[va]=q97; p107[va]=q107
83+
folds.append({'fold':k,'rows':int(len(va)),'unseen_fraction':float(uns.mean()),'v97':ll(y[va],q97),'v107':ll(y[va],q107),'gain':ll(y[va],q97)-ll(y[va],q107)})
84+
print(name,folds[-1],flush=True)
85+
rec={'v97':ll(y,p97),'v107':ll(y,p107),'gain_vs_v97':ll(y,p97)-ll(y,p107),'unseen_fraction':float(U.mean()),'folds':folds}
86+
out['worlds'][name]=rec; gains.append(rec['gain_vs_v97']); print(name,'SUMMARY',rec,flush=True)
87+
g=np.asarray(gains,float); mixed=out['worlds']['mixed_support']['gain_vs_v97']; session=out['worlds']['session_cold']['gain_vs_v97']; objg=out['worlds']['objective_cold']['gain_vs_v97']
88+
promote=(g.mean()>=.0005 and mixed>=.0003 and session>=.0003 and objg>=-.0002 and g.min()>=-.0003)
89+
out['decision']={'mean_gain_vs_v97':float(g.mean()),'mixed_support_gain':float(mixed),'session_cold_gain':float(session),'objective_cold_gain':float(objg),'worst_gain':float(g.min()),'verdict':'PROMOTE_V107_RUNTIME' if promote else 'KEEP_V97','precommit':'promote iff mean >= .0005, mixed >= .0003, session >= .0003, objective >= -.0002, worst >= -.0003'}
90+
Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2),flush=True)
91+
92+
if __name__=='__main__':
93+
p=argparse.ArgumentParser(); p.add_argument('--features',type=Path,required=True); p.add_argument('--labels',type=Path,required=True); p.add_argument('--transcripts',type=Path,required=True); p.add_argument('--out',default='v107_support_conditioned_prior.json'); run(p.parse_args())

0 commit comments

Comments
 (0)