wuff-mann commited on
Commit
91186b4
·
verified ·
1 Parent(s): fb13cff

Archive CIDM-v2 reproducible b2_r1_flux_control.py

Browse files
cidm_v2/source/B2_R1_FIX2/b2_r1_flux_control.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CIDM-v2 B2/R1: model-free flux stability qualification, then matched tiny training.
2
+ No new parameters; no test-driven threshold tuning. Source remains B0-R1 archived v1 + PDE-ported v2.
3
+ """
4
+ from __future__ import annotations
5
+ import os, io, json, math, zipfile, hashlib, random, time, copy, csv
6
+ from pathlib import Path
7
+ from dataclasses import asdict
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ from benchmark import Config, TASKS, TASKS as ALL_TASKS, fetch_official, make_cache, Windows, UnifiedModel, ARCHIVE_SHA, sha, seed_all, load_data, assess as canonical_assess
13
+
14
+ TASKLIST=('burgers_1d','reacdiff_2d','swe_2d')
15
+ SEEDS=(17,41,73)
16
+ POLICIES=('baseline','flux_cap2','flux_cap4','flux_cap8','flux_scale075','flux_scale050')
17
+
18
+ def flux_rms(t):
19
+ return t.square().mean(dim=(1,2,3),keepdim=True).clamp_min(1e-20).sqrt()
20
+
21
+ class ControlledV2(nn.Module):
22
+ """Same pretrained/newly trained v2 parameters, only altered latent Flux time stepping."""
23
+ def __init__(self, core, policy='baseline'):
24
+ super().__init__()
25
+ if policy not in POLICIES: raise ValueError(policy)
26
+ self.core=core
27
+ self.policy=policy
28
+ def forward(self, h, steps=12, return_trace=False):
29
+ is1d=h.ndim==4
30
+ h5=h.unsqueeze(-2) if is1d else h
31
+ mm=self.core.model
32
+ B,L,C,H,W=h5.shape
33
+ buf=list(h5.unbind(1))
34
+ z=mm.encode(h5.reshape(B,L*C,H,W))
35
+ coeff=mm.coefficients()
36
+ skew=mm.skew-mm.skew.T
37
+ f0=coeff['flux']*mm.flux(z)
38
+ initnorm=flux_rms(f0).detach().clamp_min(1e-10)
39
+ ys=[];tr=[]
40
+ for k in range(steps):
41
+ raw=coeff['flux']*mm.flux(z)
42
+ if self.policy.startswith('flux_cap'):
43
+ cap=float(self.policy.replace('flux_cap',''))
44
+ factor=(cap*initnorm/(flux_rms(raw)+1e-12)).clamp(max=1.)
45
+ f=raw*factor
46
+ elif self.policy.startswith('flux_scale'):
47
+ factor=float(self.policy.replace('flux_scale',''))/100.
48
+ f=raw*factor
49
+ else:
50
+ f=raw
51
+ local=coeff['local']*torch.tanh(mm.local(z))
52
+ skew_update=.035*torch.einsum('ij,bjhw->bihw',skew,z)
53
+ z=z+f+local+skew_update
54
+ logits=mm.decode(z)
55
+ inc=coeff['out']*torch.tanh(logits)
56
+ x=buf[-1]+inc
57
+ ys.append(x);buf.append(x)
58
+ if return_trace:
59
+ with torch.no_grad():
60
+ tr.append(dict(lead=k+1,raw_flux_rms=float(flux_rms(raw).mean()),
61
+ used_flux_rms=float(flux_rms(f).mean()),
62
+ local_rms=float(flux_rms(local).mean()),
63
+ decoder_logit_abs_gt2=float((logits.abs()>2).float().mean()),
64
+ output_increment_rms=float(inc.square().mean().sqrt())))
65
+ y=torch.stack(ys,dim=1)
66
+ return ((y.squeeze(-2) if is1d else y),tr) if return_trace else (y.squeeze(-2) if is1d else y)
67
+
68
+
69
+ def rms_rmse(yhat,y,sd):
70
+ return (((yhat-y)*sd).square().sum().item(),yhat.numel())
71
+
72
+ @torch.no_grad()
73
+ def assess(core,policy,cache,split,device,steps=12,batch=None,max_windows=None,details=False):
74
+ if batch is None: batch = 8 if cache['splits'][split]['h'].ndim==4 else 2 # MUST match archived B0 batch for 1D vs 2D
75
+ w=Windows(cache['splits'][split]['h'],cache['splits'][split]['y'],cache['mean'],cache['std'])
76
+ net=ControlledV2(core,policy).to(device).eval()
77
+ sq=0.;n=0; trace=[]; seq_err=None; seq_n=0; dirs={1:[],6:[],12:[]};powers={1:[],6:[],12:[]}
78
+ max_windows=min(len(w),max_windows) if max_windows else len(w)
79
+ for i in range(0,max_windows,batch):
80
+ xx=[w[j] for j in range(i,min(max_windows,i+batch))]
81
+ h=torch.stack([v[0] for v in xx]).to(device);y=torch.stack([v[1] for v in xx]).to(device)
82
+ result=net(h,steps,return_trace=details)
83
+ p,tt=result if details else (result,[])
84
+ s= w.sd.to(device)
85
+ a,b=rms_rmse(p,y[:,:steps],s);sq+=a;n+=b
86
+ if details:
87
+ for t in tt:trace.append(t)
88
+ hh=h.unsqueeze(-2) if h.ndim==4 else h
89
+ yy=y.unsqueeze(-2) if y.ndim==4 else y
90
+ pp=p.unsqueeze(-2) if p.ndim==4 else p
91
+ for k in dirs:
92
+ trueinc=yy[:,k-1]-(hh[:,-1] if k==1 else yy[:,k-2]);predinc=pp[:,k-1]-(hh[:,-1] if k==1 else pp[:,k-2]);
93
+ cos=F.cosine_similarity(predinc.flatten(1),trueinc.flatten(1),dim=1,eps=1e-12)
94
+ dirs[k].extend([float(x) for x in cos])
95
+ powers[k].append(float(predinc.square().mean().clamp_min(1e-20)/trueinc.square().mean().clamp_min(1e-20)))
96
+ out={'rmse12':math.sqrt(sq/max(n,1)),'mse12':sq/max(n,1),'num_windows':max_windows}
97
+ if details:
98
+ out.update({f'cos_lead{k}':float(np.mean(v)) for k,v in dirs.items()})
99
+ out.update({f'power_lead{k}':float(np.mean(v)) for k,v in powers.items()})
100
+ out['trace_lead12']={j:float(np.mean([t[j] for t in trace if t['lead']==12])) for j in ('raw_flux_rms','used_flux_rms','local_rms','decoder_logit_abs_gt2','output_increment_rms')}
101
+ return out
102
+
103
+
104
+ def get_core(z,cache,task,seed,device):
105
+ name=f'best_models/{task}__V2_Ported_R5__seed{seed}.pt'
106
+ payload=z.read(name)
107
+ ck=torch.load(io.BytesIO(payload),map_location='cpu',weights_only=True)
108
+ assert ck['source_sha']==ARCHIVE_SHA and ck['seed']==seed and ck['model_kind']=='V2_Ported_R5'
109
+ cfg=ck['config'];channel=cache['splits']['train']['h'].shape[2]
110
+ core=UnifiedModel('V2_Ported_R5',channel,cfg['d'],cfg['hidden'],4).to(device)
111
+ core.load_state_dict(ck['model'],strict=True)
112
+ core.eval()
113
+ return core,hashlib.sha256(payload).hexdigest()
114
+
115
+ def check_zip(z):
116
+ assert z.testzip() is None
117
+ r=json.loads(z.read('B0_REPORT.json'));gate=r['gate'];c=r['config']
118
+ assert c['profile']=='pilot_r1' and c['eval_horizon']==12 and c['train_horizon']==6
119
+ assert gate['completed_runs']==18 and gate['official_data'] and gate['all_complete']
120
+ for task in TASKLIST:
121
+ for seed in SEEDS:
122
+ assert f'best_models/{task}__V2_Ported_R5__seed{seed}.pt' in z.namelist()
123
+ return r,json.loads(z.read('data_and_architecture_contract.json'))
124
+
125
+ def output_csv(path,rows):
126
+ if not rows:return
127
+ with open(path,'w',newline='',encoding='utf-8') as f:
128
+ wr=csv.DictWriter(f,fieldnames=list(rows[0]));wr.writeheader();wr.writerows(rows)
129
+
130
+ def gate_validate(rows):
131
+ """One policy globally selected across all three PDEs; no family-dependent runtime routing."""
132
+ cand=[]
133
+ baseline={(r['task'],r['seed']):r['mse12'] for r in rows if r['policy']=='baseline'}
134
+ for policy in POLICIES[1:]:
135
+ x=[r for r in rows if r['policy']==policy]
136
+ ratios={(r['task'],r['seed']):r['mse12']/baseline[(r['task'],r['seed'])] for r in x}
137
+ family={task:float(np.mean([ratios[(task,s)] for s in SEEDS])) for task in TASKLIST}
138
+ seed_wins={task:sum(ratios[(task,s)]<1 for s in SEEDS) for task in TASKLIST}
139
+ gm=float(math.exp(np.mean([math.log(v) for v in family.values()])))
140
+ eligible=(gm<=.97 and all(v<=1.02 for v in family.values()) and all(v>=2 for v in seed_wins.values()))
141
+ cand.append(dict(policy=policy,geometric_mean_mse_ratio=gm,worst_family_ratio=max(family.values()),
142
+ family_ratios=family,seed_wins=seed_wins,validation_qualified=bool(eligible)))
143
+ eligible=sorted((t for t in cand if t['validation_qualified']),key=lambda r:r['geometric_mean_mse_ratio'])
144
+ return {'chosen_policy':eligible[0]['policy'] if eligible else None,
145
+ 'state':'FROZEN_VAL_QUALIFIED_FOR_TRAINING_PREFLIGHT' if eligible else 'FROZEN_VAL_GATE_FAIL_NO_TRAINING',
146
+ 'requirements':{'global_geometric_mse_gain_min':.03,'max_each_family_mse_regression':.02,
147
+ 'min_paired_seed_wins_per_family':2},'candidates':cand}
148
+
149
+ def _seed(seed):
150
+ random.seed(seed);np.random.seed(seed);torch.manual_seed(seed)
151
+ if torch.cuda.is_available():torch.cuda.manual_seed_all(seed)
152
+
153
+ def train_pair(task,cache,seed,policy,outdir,device,updates=240,smoke=False):
154
+ """Same initial parameters, same window indices, same training budget. Qualification only."""
155
+ from benchmark import choose_match
156
+ cfg=Config(profile='pilot_r1')
157
+ channel=cache['splits']['train']['h'].shape[2]
158
+ # Exact prior B0 parameter specs from checkpoint are ideal but same parameter count within this pair suffices.
159
+ conf=choose_match('V2_Ported_R5',channel,cfg) if not smoke else {'d':8,'hidden':12}
160
+ _seed(seed)
161
+ init=UnifiedModel('V2_Ported_R5',channel,conf['d'],conf['hidden'],4)
162
+ sd=copy.deepcopy(init.state_dict()); del init
163
+ dataset=Windows(cache['splits']['train']['h'],cache['splits']['train']['y'],cache['mean'],cache['std'])
164
+ logs=[];results=[]
165
+ for route in ('baseline',policy):
166
+ core=UnifiedModel('V2_Ported_R5',channel,conf['d'],conf['hidden'],4).to(device)
167
+ core.load_state_dict(sd,strict=True)
168
+ net=ControlledV2(core,route).to(device)
169
+ optimizer=torch.optim.AdamW(net.parameters(),lr=1e-3,weight_decay=1e-4)
170
+ best=float('inf');best_state=None;hist=[]
171
+ snapshot=Path(outdir)/f'Q1_LATEST_{task}__{route}__seed{seed}.pt'
172
+ start_step=0
173
+ if snapshot.exists():
174
+ saved=torch.load(snapshot,map_location=device,weights_only=True)
175
+ if (saved.get('seed')==seed and saved.get('route')==route and
176
+ saved.get('candidate_policy')==policy and saved.get('updates')==updates and
177
+ saved.get('match_spec')==conf):
178
+ net.load_state_dict(saved['model'],strict=True)
179
+ optimizer.load_state_dict(saved['optimizer'])
180
+ start_step=int(saved['step']);best_state=saved['best_model']
181
+ hist=saved['history'];best=float(saved['best_rmse12'])
182
+ print('[Q1 RESUME]',task,route,seed,'step',start_step,flush=True)
183
+ for step in range(start_step,updates):
184
+ net.train()
185
+ inds=torch.randint(0,len(dataset),(2 if len(cache['splits']['train']['h'].shape)>4 else 8,),
186
+ generator=torch.Generator().manual_seed(seed*1000003+step)).tolist()
187
+ samples=[dataset[i] for i in inds]
188
+ h=torch.stack([x[0] for x in samples]).to(device)
189
+ y=torch.stack([x[1] for x in samples]).to(device)
190
+ optimizer.zero_grad(set_to_none=True)
191
+ p=net(h,6);loss=F.mse_loss(p,y[:,:6]);assert torch.isfinite(loss)
192
+ loss.backward();nn.utils.clip_grad_norm_(net.parameters(),1.);optimizer.step()
193
+ if (step+1)%max(2,updates//4)==0 or step+1==updates:
194
+ ev=assess(net.core,route,cache,'val',device,batch=2,max_windows=2 if smoke else None)
195
+ if ev['rmse12']<best:
196
+ best=ev['rmse12'];best_state={k:v.detach().cpu().clone() for k,v in net.state_dict().items()}
197
+ rr=dict(task=task,seed=seed,route=route,step=step+1,train_mse=float(loss.detach()),
198
+ val_rmse12=ev['rmse12'],best_val_rmse12=best)
199
+ hist.append(rr);print('[Q1 train]',rr,flush=True)
200
+ # Persist last and best weights/optimizer for diagnosis even on interrupted runs.
201
+ snapshot_tmp=snapshot.with_suffix('.pt.tmp')
202
+ torch.save({'model':net.state_dict(),'best_model':best_state,'optimizer':optimizer.state_dict(),
203
+ 'step':step+1,'history':hist,'seed':seed,'route':route,
204
+ 'candidate_policy':policy,'qualified_only':True,
205
+ 'best_rmse12':best,'updates':updates,'match_spec':conf},snapshot_tmp)
206
+ os.replace(snapshot_tmp,snapshot)
207
+ # Optional HF checkpoint + GitHub small progress report; no failure may stop scientific training.
208
+ if os.getenv('CIDM_LIVE_SYNC','1')=='1':
209
+ try:
210
+ from artifact_store import sync_training_milestone
211
+ ret=sync_training_milestone(snapshot,rr,
212
+ repo_id=os.getenv('CIDM_HF_RESULTS_REPO',''),
213
+ github_repo=os.getenv('CIDM_GITHUB_REPO','Mr-wuff/AI-Weather-Quest'))
214
+ print('[Q1 ARCHIVE]',ret,flush=True)
215
+ except Exception as archive_exc:
216
+ print('[Q1 ARCHIVE NONBLOCKING ERROR]',str(archive_exc)[:140],flush=True)
217
+
218
+ net.load_state_dict(best_state)
219
+ outfile=Path(outdir)/f'Q1_{task}__{route}__seed{seed}.pt'
220
+ torch.save({'model_state':best_state,'route':route,'task':task,'seed':seed,'updates':updates,
221
+ 'match_spec':conf,'qualification_only':True},outfile)
222
+ results.append(dict(task=task,seed=seed,route=route,updates=updates,best_val_rmse12=best,
223
+ train_history=hist,checkpoint=outfile.name))
224
+ logs+=hist
225
+ return results,logs
226
+
227
+ def run(b0zip,outdir,root='/content/CIDM_v2_B0_FairPDE',smoke=False):
228
+ torch.set_num_threads(min(4,torch.get_num_threads()))
229
+ out=Path(outdir);out.mkdir(parents=True,exist_ok=True)
230
+ assert sha(Path(__file__).with_name('cidm_complete_frozen.py'))==ARCHIVE_SHA
231
+ z=zipfile.ZipFile(b0zip);r0,manifest=check_zip(z)
232
+ device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
233
+ rows=[];replay=[];caches={};diagnostic_rows=[]
234
+ tasks=('swe_2d',) if smoke else TASKLIST
235
+ seeds=(17,) if smoke else SEEDS
236
+ for task in tasks:
237
+ if smoke:
238
+ g=torch.Generator().manual_seed(2033)
239
+ a=torch.randn(4,16,1,8,16,generator=g)*.03
240
+ for t in range(1,16):a[:,t]=.98*a[:,t-1]+.02*a[:,t]
241
+ caches[task]={'splits':{k:{'h':a[:,:4], 'y':a[:,4:]} for k in ('train','val','test')},
242
+ 'mean':torch.zeros(1),'std':torch.ones(1)}
243
+ else:
244
+ cfg=Config(**r0['config']);data_dir=Path(os.getenv('CIDM_B2_DATA_ROOT',str(Path(root)/'official_data')))
245
+ official=fetch_official(data_dir,task)
246
+ caches[task]=make_cache(task,official,cfg,Path(root)/'window_cache')
247
+ ref=next(t for t in manifest['tasks'] if t['task']==task)
248
+ assert caches[task]['signature']['source_md5']==ref['source_md5']
249
+ # Check trajectory IDs (not just window counts / source MD5). A stale cache can otherwise silently pass.
250
+ assert caches[task]['ids']==ref['ids'], f'{task}: OFFICIAL_TRAJECTORY_ID_MISMATCH'
251
+ for split in ('train','val','test'):
252
+ assert len(caches[task]['splits'][split]['h'])==ref['windows'][split]
253
+ for seed in seeds:
254
+ core,hsh=get_core(z,caches[task],task,seed,device)
255
+ base=assess(core,'baseline',caches[task],'val',device,batch=2,max_windows=2 if smoke else None)
256
+ if not smoke:
257
+ # REPLAY FIX2: archival benchmark loader, exact batch size, original forward FIRST.
258
+ # A different batch size may cause divergent GPU convolution rounding during unstable rollout.
259
+ cfg=Config(**r0['config'])
260
+ original_batch=cfg.batch_1d if caches[task]['splits']['test']['h'].ndim==4 else cfg.batch_2d
261
+ orig=float(json.loads(z.read(f'{task}__V2_Ported_R5__seed{seed}.json'))['test']['model_12'])
262
+ ref_loader=load_data(caches[task],'test',original_batch,seed,False)
263
+ canonical=canonical_assess(core,ref_loader,device,cfg,mode='off')['model_12']
264
+ # Run the wrapper with the SAME batch schedule and normalization.
265
+ actual=assess(core,'baseline',caches[task],'test',device,batch=original_batch)
266
+ # Check element-wise equality, in the original B0 test batch shape.
267
+ hw,yy=next(iter(ref_loader));hw=hw.to(device)
268
+ with torch.no_grad():
269
+ raw_out=core(hw,12,'off')
270
+ wrapped_out=ControlledV2(core,'baseline')(hw,12)
271
+ parity=float((raw_out-wrapped_out).abs().max())
272
+ tol=max(1e-5,orig*.001)
273
+ row=dict(task=task,seed=seed,checkpoint_sha256=hsh,archival_batch=original_batch,
274
+ original_rmse12=orig,canonical_rmse12=canonical,wrapped_rmse12=actual['rmse12'],
275
+ max_forward_parity_diff=parity,
276
+ original_difference=canonical-orig,wrapper_difference=actual['rmse12']-canonical,
277
+ pass_archive=bool(abs(canonical-orig)<=tol),
278
+ pass_wrapper=bool(abs(actual['rmse12']-canonical)<=tol and parity<2e-6))
279
+ replay.append(row)
280
+ if not row['pass_archive'] or not row['pass_wrapper']:
281
+ # Preserve exact evidence before fail-closed. Never enter Q0 on a mismatched baseline.
282
+ (out/'REPLAY_FAILURE_DIAGNOSTIC.json').write_text(json.dumps({'reason':'CANONICAL_SOURCE_OR_WRAPPER_REPLAY_MISMATCH',
283
+ 'failed_run':row,'prior_runs':replay,'data_signature':caches[task]['signature'],
284
+ 'historical_report_config':r0['config']},indent=2,ensure_ascii=False))
285
+ raise RuntimeError('B0 replay FAIL-CLOSED: '+json.dumps(row,ensure_ascii=False)+
286
+ '. See REPLAY_FAILURE_DIAGNOSTIC.json. Confirm checkpoint/data/cache/batch. DO NOT relax tolerance.')
287
+ print('[B0 archived exact replay PASS]',task,seed,'batch',original_batch,
288
+ 'archived',round(orig,8),'reference',round(canonical,8),
289
+ 'wrapper',round(actual['rmse12'],8),'max_diff',parity,flush=True)
290
+ # Verify unchanged baseline replay exactly against original architecture.
291
+ w=Windows(caches[task]['splits']['val']['h'],caches[task]['splits']['val']['y'],
292
+ caches[task]['mean'],caches[task]['std'])
293
+ h=torch.stack([w[0][0]]).to(device)
294
+ with torch.no_grad():
295
+ diff=float((ControlledV2(core,'baseline')(h,12)-core(h,12,'off')).abs().max())
296
+ assert diff<2e-6,(task,seed,diff)
297
+ print('[B2 R1 Q0]',task,'seed',seed,'baseline val12',base['rmse12'])
298
+ # Causally available validation histories only; measure saturation and direction
299
+ # on a small subset. These observations do not affect policy selection.
300
+ dp=assess(core,'baseline',caches[task],'val',device,batch=2,max_windows=2 if smoke else 8,details=True)
301
+ diagnostic_rows.append(dict(task=task,seed=seed,policy='baseline',
302
+ **{k:v for k,v in dp.items() if k.startswith(('cos_lead','power_lead'))},
303
+ **dp['trace_lead12']))
304
+ for policy in POLICIES:
305
+ x=base if policy=='baseline' else assess(core,policy,caches[task],'val',device,batch=2,max_windows=2 if smoke else None)
306
+ rows.append(dict(task=task,seed=seed,policy=policy,mse12=x['mse12'],rmse12=x['rmse12'],
307
+ gain_vs_baseline_pct=100*(1-x['mse12']/base['mse12']),val_windows=x['num_windows']))
308
+ del core
309
+ if device.type=='cuda':torch.cuda.empty_cache()
310
+ output_csv(out/'frozen_validation_selection.csv',rows)
311
+ output_csv(out/'baseline_validation_operator_diagnostics.csv',diagnostic_rows)
312
+ stage={'state':'PROGRAM_ONLY_SMOKE' if smoke else 'UNKNOWN','chosen_policy':None,'candidates':[]}
313
+ if not smoke:stage=gate_validate(rows)
314
+ q1results=[];history=[]
315
+ if stage['chosen_policy'] is not None and not smoke:
316
+ print('[B2 R1] Frozen validated candidate:',stage['chosen_policy'], 'entering TRAINED Q1 seed17 only')
317
+ for task in TASKLIST:
318
+ ans,logs=train_pair(task,caches[task],17,stage['chosen_policy'],out,device,updates=240)
319
+ q1results+=ans;history+=logs
320
+ ref={r['task']:r for r in q1results if r['route']=='baseline'}
321
+ gains={r['task']:1-r['best_val_rmse12']/ref[r['task']]['best_val_rmse12']
322
+ for r in q1results if r['route']!= 'baseline'}
323
+ # First-stage tests only, not automatic multi-seed formal training.
324
+ q1pass=all(gains[t]>=.02 for t in ('burgers_1d','swe_2d')) and gains['reacdiff_2d']>=-.02
325
+ stage['q1_trainable_prequalification']={'improvement_by_task':gains,'pass':q1pass,
326
+ 'state':'Q1_SINGLE_SEED_PREQUALIFIED_FURTHER_SEEDS_NEEDED' if q1pass else 'Q1_STOP_BEFORE_FORMAL_TRAINING'}
327
+ stage['state']='Q1_SINGLE_SEED_PREQUALIFIED_FURTHER_SEEDS_NEEDED' if q1pass else 'Q1_STOP_BEFORE_FORMAL_TRAINING'
328
+ report={'status':stage['state'],'data':'SYNTHETIC_SMOKE' if smoke else 'OFFICIAL_MD5_CHECKED',
329
+ 'original_B0_completed_runs':r0['gate']['completed_runs'],
330
+ 'original_B0_source':ARCHIVE_SHA,'frozen_gate':stage,
331
+ 'test_replay_only_not_used_for_selection':replay,'trainable_q1_results':q1results,
332
+ 'limitations':['Q0 hyperparameter selection uses validation only; official test used for original replay check only.',
333
+ 'Q1 is 240-update seed17 trained qualification, NOT cross-seed scientific superiority.',
334
+ 'Policy changes learned R5 transition and may distort a frozen checkpoint; no inference policy is adopted without retrained comparison.',
335
+ 'Flux latent telescoping is not physical energy conservation; perturbation growth is not a Lyapunov exponent.',
336
+ 'Original three PDE families have repeatedly informed development; not truly blind transfer.']}
337
+ (out/'report.json').write_text(json.dumps(report,indent=2,ensure_ascii=False),encoding='utf-8')
338
+ output_csv(out/'q1_training_history.csv',history)
339
+ resultzip=out/'CIDM_v2_B2_R1_FLUX_CONTROL_QUALIFICATION_REPORTS.zip'
340
+ with zipfile.ZipFile(resultzip,'w',compression=zipfile.ZIP_DEFLATED) as zout:
341
+ for file in sorted(out.glob('*')):
342
+ if file!=resultzip and file.is_file():zout.write(file,arcname=file.name)
343
+ assert zipfile.ZipFile(resultzip).testzip() is None
344
+ z.close();return report,str(resultzip)