Skip to content

Commit 1e1a3ab

Browse files
committed
trace ace: add V105 prior-state composition reset
1 parent 9c033cf commit 1e1a3ab

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
"""V105: restore the missing V74 objective-prior + V75 student-state composition.
3+
4+
Repo reset hypothesis: V74 was promoted as a mandatory independent objective-difficulty
5+
prior, but the V75->V104 lineage largely rebuilt V75 without it. Test the smallest
6+
lawful composition before further routing work.
7+
8+
For each outer validation world:
9+
* V74 is fit only on outer-train and predicts outer-valid.
10+
* V75 and RELATED are fit only on outer-train and predict outer-valid.
11+
* Inner grouped OOF predictions on outer-train select a tiny convex grid.
12+
* RELATED weight is permitted only where the exact objective has zero support in the
13+
corresponding training fold. No test-batch aggregation or cross-response features.
14+
15+
A deterministic mixed-support world combines unseen-objective rows with held-out-session
16+
rows on otherwise seen objectives, so the gate is not judged only in pure objective-cold.
17+
"""
18+
from __future__ import annotations
19+
import argparse, json
20+
from pathlib import Path
21+
import numpy as np
22+
from sklearn.cluster import KMeans
23+
from sklearn.linear_model import LogisticRegression
24+
from sklearn.metrics import log_loss
25+
from sklearn.model_selection import GroupKFold
26+
27+
from v71_mastery_events import load_transcript
28+
from v74_semantic_objective_prior import semantic_prior_predict
29+
from v75_canonical_trajectory import load_training, SEED
30+
from v85_evidence_state import build_v75
31+
from v93_shift_robust_validation import folds_from_groups, obj_family, style_matrix
32+
from v94_related_control import segmented_control, build_control
33+
34+
EPS=1e-5
35+
W74=np.array([0.,.10,.20,.30,.40,.50])
36+
WR=np.array([0.,.10,.20,.30,.40])
37+
38+
39+
def ll(y,p): return float(log_loss(y,np.clip(p,EPS,1-EPS),labels=[0,1]))
40+
41+
def fit_lr(X,y,tr,va):
42+
m=LogisticRegression(C=.25,max_iter=300,solver='liblinear',random_state=SEED).fit(X[tr],y[tr])
43+
return np.clip(m.predict_proba(X[va])[:,1],EPS,1-EPS)
44+
45+
def unseen_mask(keys,tr,va):
46+
seen=set(keys[tr].tolist())
47+
return np.asarray([keys[i] not in seen for i in va],bool)
48+
49+
def compose(p75,p74,pr,unseen,w74,wr):
50+
# RELATED is unavailable on supported objectives. Preserve convexity per row.
51+
rw=np.where(unseen,wr,0.0)
52+
base=np.maximum(0.0,1.0-w74-rw)
53+
return np.clip(base*p75+w74*p74+rw*pr,EPS,1-EPS)
54+
55+
def mixed_support_folds(obj,sess,n=5):
56+
"""Each fold has cold objectives plus session-held-out rows from remaining objectives."""
57+
uo=np.unique(obj); us=np.unique(sess)
58+
of={x:i % n for i,x in enumerate(sorted(uo))}
59+
sf={x:i % n for i,x in enumerate(sorted(us))}
60+
out=[]
61+
idx=np.arange(len(obj))
62+
for k in range(n):
63+
cold=np.asarray([of[x]==k for x in obj])
64+
seen_session=np.asarray([(of[o]!=k and sf[s]==k) for o,s in zip(obj,sess)])
65+
va=idx[cold|seen_session]; tr=idx[~(cold|seen_session)]
66+
out.append((tr,va))
67+
return out
68+
69+
def inner_oof(f,X75,Xr,y,outer_tr,groups,support_key):
70+
n=len(outer_tr); p75=np.zeros(n); p74=np.zeros(n); pr=np.zeros(n); uns=np.zeros(n,bool)
71+
g=groups[outer_tr]
72+
ns=min(3,len(np.unique(g)))
73+
for itr_l,iva_l in GroupKFold(ns).split(np.zeros(n),y[outer_tr],g):
74+
itr=outer_tr[itr_l]; iva=outer_tr[iva_l]
75+
p75[iva_l]=fit_lr(X75,y,itr,iva)
76+
pr[iva_l]=fit_lr(Xr,y,itr,iva)
77+
p74[iva_l],_=semantic_prior_predict(f.iloc[itr],f.iloc[iva])
78+
uns[iva_l]=unseen_mask(support_key,itr,iva)
79+
return p75,p74,pr,uns
80+
81+
def select_weights(y,p75,p74,pr,uns):
82+
best_base=(1e99,None); best_gate=(1e99,None)
83+
for a in W74:
84+
q=compose(p75,p74,pr,uns,a,0.0); v=ll(y,q)
85+
if v<best_base[0]: best_base=(v,{'w74':float(a),'wr':0.0})
86+
for r in WR:
87+
if a+r>.80: continue
88+
q=compose(p75,p74,pr,uns,a,r); v=ll(y,q)
89+
if v<best_gate[0]: best_gate=(v,{'w74':float(a),'wr':float(r)})
90+
return best_base,best_gate
91+
92+
def run(a):
93+
f=load_training(a.features,a.labels).reset_index(drop=True)
94+
cache={sid:load_transcript(a.transcripts/f'{sid}.csv') for sid in f.session_id.astype(str).unique()}
95+
rt=[]; rz=[]
96+
for i,r in f.iterrows():
97+
t,z=segmented_control(cache[str(r.session_id)],str(r.learning_objective),'related'); rt.append(t); rz.append(z)
98+
if (i+1)%2500==0: print('rows',i+1,flush=True)
99+
X75=build_v75(f,cache); Xr=build_control(rt,rz); y=f.target.to_numpy(int)
100+
obj=(f.learning_objective_id if 'learning_objective_id' in f else f.learning_objective).astype(str).to_numpy()
101+
support=f.learning_objective.astype(str).to_numpy(); sess=f.session_id.astype(str).to_numpy()
102+
fam=f.learning_objective.astype(str).map(obj_family).astype(str).to_numpy()
103+
style=KMeans(n_clusters=5,random_state=137,n_init=10).fit(style_matrix(f,cache)).labels_.astype(str)
104+
worlds={
105+
'objective_cold':(folds_from_groups(obj),obj),
106+
'session_cold':(folds_from_groups(sess),sess),
107+
'objective_family_cold':(folds_from_groups(fam),fam),
108+
'style_cold':(folds_from_groups(style),style),
109+
'mixed_support':(mixed_support_folds(obj,sess),obj),
110+
}
111+
out={'primary':'restore V74 objective prior + V75 state + lawful unsupported RELATED','worlds':{}}
112+
gains=[]; gate_gains=[]
113+
for name,(sp,inner_groups) in worlds.items():
114+
P75=np.zeros(len(y)); P74=np.zeros(len(y)); PG=np.zeros(len(y)); PB=np.zeros(len(y)); PR=np.zeros(len(y)); U=np.zeros(len(y),bool)
115+
folds=[]
116+
for k,(tr,va) in enumerate(sp,1):
117+
p75=fit_lr(X75,y,tr,va); pr=fit_lr(Xr,y,tr,va); p74,_=semantic_prior_predict(f.iloc[tr],f.iloc[va])
118+
uns=unseen_mask(support,tr,va)
119+
i75,i74,ir,iu=inner_oof(f,X75,Xr,y,tr,inner_groups,support)
120+
base,gate=select_weights(y[tr],i75,i74,ir,iu)
121+
qb=compose(p75,p74,pr,uns,base[1]['w74'],0.0)
122+
qg=compose(p75,p74,pr,uns,gate[1]['w74'],gate[1]['wr'])
123+
P75[va]=p75; P74[va]=p74; PR[va]=pr; PB[va]=qb; PG[va]=qg; U[va]=uns
124+
folds.append({'fold':k,'rows':int(len(va)),'unseen_fraction':float(uns.mean()),
125+
'v75':ll(y[va],p75),'v74':ll(y[va],p74),'prior_state':ll(y[va],qb),'prior_state_related':ll(y[va],qg),
126+
'selected_prior':base[1],'selected_gate':gate[1]})
127+
print(name,folds[-1],flush=True)
128+
rec={'v75':ll(y,P75),'v74':ll(y,P74),'related':ll(y,PR),'prior_state':ll(y,PB),'prior_state_related':ll(y,PG),
129+
'gain_prior_state_vs_v75':ll(y,P75)-ll(y,PB),'gain_gate_vs_v75':ll(y,P75)-ll(y,PG),
130+
'gain_gate_vs_prior_state':ll(y,PB)-ll(y,PG),'unseen_fraction':float(U.mean()),'folds':folds}
131+
out['worlds'][name]=rec; gains.append(rec['gain_prior_state_vs_v75']); gate_gains.append(rec['gain_gate_vs_v75'])
132+
print(name,'SUMMARY',rec,flush=True)
133+
# Promotion is based on broad lawful transfer, not objective-cold alone.
134+
key=['session_cold','objective_cold','objective_family_cold','style_cold','mixed_support']
135+
g=np.asarray([out['worlds'][x]['gain_gate_vs_v75'] for x in key])
136+
gp=np.asarray([out['worlds'][x]['gain_prior_state_vs_v75'] for x in key])
137+
mixed=out['worlds']['mixed_support']['gain_gate_vs_v75']; session=out['worlds']['session_cold']['gain_gate_vs_v75']
138+
promote=(g.mean()>=.0015 and mixed>=.0010 and session>=-.0005 and g.min()>=-.0010)
139+
out['decision']={'mean_gate_gain':float(g.mean()),'mean_prior_state_gain':float(gp.mean()),'mixed_support_gain':float(mixed),
140+
'worst_gate_gain':float(g.min()),'verdict':'PROMOTE_V105_COMPOSITION' if promote else 'DO_NOT_PROMOTE_V105',
141+
'precommit':'promote iff mean five-world gate gain >= .0015, mixed-support >= .0010, session-cold >= -.0005, no world worse than -.0010'}
142+
Path(a.out).write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2),flush=True)
143+
if __name__=='__main__':
144+
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='v105_prior_state_composition.json'); run(p.parse_args())

0 commit comments

Comments
 (0)