File size: 7,119 Bytes
547ac30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Train RSW and TIG experts independently, starting from randomized weights.
Select checkpoint and post-model blending on validation ONLY.
"""
import argparse,copy,csv,json,random,time
from pathlib import Path
import numpy as np,torch
from torch.nn import functional as F
from welding_specialists import WeldingSet,WeldingMoE55,predict
ROOT=Path(__file__).resolve().parents[1]
COMP=['robot_joint_drives_Wh','robot_controller_Wh','process_power_source_Wh','process_actuator_Wh','cooling_Wh','fume_extraction_Wh','gas_valve_Wh','source_standby_Wh','external_axes_Wh']
Y=['time_s','energy_Wh','material_added_g','wire_consumed_g','gas_consumed_L','spot_count']

def metrics(y,p,zero_policy='ignore'):
 y=np.asarray(y,float);p=np.asarray(p,float);error=p-y
 nz=np.abs(y)>1e-9
 return {'count':int(len(y)),'MAPE_pct':(float(np.mean(np.abs(error[nz]/y[nz]))*100) if np.any(nz) else None),
 'MAPE_nonzero_n':int(nz.sum()),'WAPE_pct':(float(np.sum(abs(error))/np.sum(abs(y))*100) if abs(y).sum()>1e-9 else None),
 'MSE':float(np.mean(error**2)),'R2':(float(1-np.sum(error**2)/np.sum((y-y.mean())**2)) if np.sum((y-y.mean())**2)>1e-9 else None),
 'MAE':float(np.mean(abs(error)))}

def load(process,sp):
 with np.load(ROOT/'data'/('resistance_spot_welding' if process=='rsw' else 'tig_welding')/f'{sp}.npz') as f:return {k:f[k] for k in f.files}

def run(process='rsw',epochs=240,patience=50,seed=5551,batch=64,device='auto'):
 torch.set_num_threads(1);np.random.seed(seed);random.seed(seed);torch.manual_seed(seed)
 device=('cuda' if torch.cuda.is_available() else 'cpu') if device=='auto' else device
 if device=='cuda' and not torch.cuda.is_available():raise RuntimeError('CUDA requested but unavailable')
 train=WeldingSet(load(process,'train'))
 sets={'train':train,**{sp:WeldingSet(load(process,sp),train.norm) for sp in ('val','test','ood')}}
 model=WeldingMoE55(train.global_x.shape[1],train.history.shape[1],process).to(device)
 opt=torch.optim.AdamW(model.parameters(),lr=.0011,weight_decay=.0001)
 J,M,G,H=[t.to(device) for t in train.tensors()];targ=torch.from_numpy(train.y_logratio).to(device);p=torch.from_numpy(train.comp_prior).to(device);yt=torch.from_numpy(train.y[:,1]).to(device);
 fill=torch.from_numpy(train.material_ratio).to(device)
 best=float('inf');best_state=None;best_ep=0;pat=0;hist=[];start=time.time()
 for ep in range(1,epochs+1):
  model.train();losses=[]
  for batchidx in torch.randperm(len(J)).split(batch):
   opt.zero_grad();z,dep=model(J[batchidx],M[batchidx],G[batchidx],H[batchidx]);z=z.clamp(-1,1)
   proposed=p[batchidx]*torch.exp(z)
   pred=proposed.sum(1)
   loge=torch.log(pred.clamp_min(1e-8)/yt[batchidx].clamp_min(1e-8))
   shares=(p[batchidx]/p[batchidx].sum(1,keepdim=True).clamp_min(1e-8)).detach()
   component_loss=(F.smooth_l1_loss(z,targ[batchidx],beta=.06,reduction='none')*(.3+shares)).mean()
   total_loss=F.smooth_l1_loss(loge,torch.zeros_like(loge),beta=.035)
   if process=='tig':
    valid=(fill[batchidx]>.01)
    material_loss=F.smooth_l1_loss(dep[valid],fill[batchidx][valid],beta=.025) if valid.any() else 0.
   else:material_loss=0.
   loss=2*total_loss+.8*component_loss+.20*material_loss
   loss.backward();torch.nn.utils.clip_grad_norm_(model.parameters(),3.);opt.step()
   losses.append(float(loss.detach()))
  pv,_=predict(model,sets['val']);en=metrics(sets['val'].y[:,1],pv[:,1]);
  material=metrics(sets['val'].y[:,2],pv[:,2]);criterion=en['MAPE_pct']+.20*en['WAPE_pct']+(.05*material['MAPE_pct'] if material['MAPE_pct'] is not None else 0)
  hist.append({'epoch':ep,'train_loss':np.mean(losses),'val_energy_MAPE_pct':en['MAPE_pct'],'val_WAPE_pct':en['WAPE_pct'],'val_added_MAPE_pct':material['MAPE_pct'],'criterion':criterion})
  if criterion<best-1e-5:best=criterion;best_state=copy.deepcopy(model.state_dict());best_ep=ep;pat=0
  else:pat+=1
  if ep==1 or ep%25==0: print(process,'epoch',ep,'val_M',round(en['MAPE_pct'],4),'val_W',round(en['WAPE_pct'],4),'best',best_ep,flush=True)
  if pat>=patience:break
 model.load_state_dict(best_state)
 # Blend choice also validation-only; fallback to physical prior when NN hurts.
 val=sets['val'];options=[]
 for blend in (0.,.25,.5,.75,1.):
  yp,_=predict(model,val,blend=blend);v=metrics(val.y[:,1],yp[:,1]);score=v['MAPE_pct']+.2*v['WAPE_pct'];options.append((score,blend))
 blend=min(options)[1]
 output=ROOT/'reports'/process
 output.mkdir(parents=True,exist_ok=True)
 results={};group=[];matched=[]
 for sp,d in sets.items():
  yp,c=predict(model,d,blend=blend)
  pure,_=predict(model,d,blend=0)
  cold,_=predict(model,d,no_history=True,blend=blend)
  m={nm:metrics(d.y[:,i],yp[:,i]) for i,nm in enumerate(Y)}
  m['physics_prior_energy']=metrics(d.y[:,1],pure[:,1]);m['history_zeroed_residual_energy']=metrics(d.y[:,1],cold[:,1]);
  m['components']={name:metrics(d.c[:,i],c[:,i]) for i,name in enumerate(COMP)}
  results[sp]=m
  for name in sorted(set(d.topology)):
   f=(d.topology==name)
   group.append({'split':sp,'topology':name,'programs':int(f.sum()),**metrics(d.y[f,1],yp[f,1])})
  if sp in ('test','ood'):
   for k in range(len(yp)):
    matched.append({'split':sp,'program_id':str(d.pids[k]),'robot_id':str(d.ids[k]),'topology':str(d.topology[k]),'n_axes':int(d.axes[k]),
                    'true_energy_Wh':float(d.y[k,1]),'prediction_energy_Wh':float(yp[k,1]),'physics_prior_Wh':float(d.prior[k,1]),
                    'true_added_g':float(d.y[k,2]),'prediction_added_g':float(yp[k,2])})
  print(process,sp,'energy MAPE',round(m['energy_Wh']['MAPE_pct'],5),'WAPE',round(m['energy_Wh']['WAPE_pct'],5),flush=True)
 with open(output/'group_metrics.csv','w',newline='') as f:
  w=csv.DictWriter(f,fieldnames=list(group[0]));w.writeheader();w.writerows(group)
 with open(output/'matched_predictions.csv','w',newline='') as f:
  w=csv.DictWriter(f,fieldnames=list(matched[0]));w.writeheader();w.writerows(matched)
 torch.save({'architecture':'WeldingMoE55','process':process,'model_state':model.state_dict(),
    'normalizer':train.norm,'global_dim':train.global_x.shape[1],'history_dim':train.history.shape[1],
    'blend':blend,'seed':seed,'best_epoch':best_ep,'from_scratch':True,'previous_weights_loaded':False},output/'model.pt')
 report={'process':process,'version':'normal','from_scratch':True,'weights_shared_with_mig_or_other_welding_models':False,
   'robot_kinematics_source':'nine welding-cell topologies','best_epoch':best_ep,'validation_blend':blend,
   'checkpoint_selection':'validation MAPE+0.2 WAPE + 0.05 material MAPE (TIG)',
   'num_parameters':sum(p.numel() for p in model.parameters()),'runtime_seconds':time.time()-start,
   'splits':results,'training_history':hist}
 (output/'metrics.json').write_text(json.dumps(report,indent=2))
 return report
if __name__=='__main__':
 p=argparse.ArgumentParser();p.add_argument('--process',choices=['rsw','tig'],required=True);p.add_argument('--epochs',type=int,default=240)
 p.add_argument('--patience',type=int,default=50);p.add_argument('--seed',type=int,default=5551);p.add_argument('--batch',type=int,default=64);p.add_argument('--device',choices=['auto','cpu','cuda'],default='auto')
 args=p.parse_args();run(**vars(args))