Download source/train_cosmo.py from Queimo/rem3di-cosmo10-combisolv-multitask: direct link, hf CLI and curl.
- Browser
- Download file 21.6 kB
-
https://huggingface.co/Queimo/rem3di-cosmo10-combisolv-multitask/resolve/main/source/train_cosmo.py
- Command line
-
hf download hf://Queimo/rem3di-cosmo10-combisolv-multitask/source/train_cosmo.py
-
curl -L -o train_cosmo.py https://huggingface.co/Queimo/rem3di-cosmo10-combisolv-multitask/resolve/main/source/train_cosmo.py
21.6 kB
| """Ten-epoch supervised COSMO continuation of the selected PCQM epoch 2.""" | |
| import argparse | |
| import fcntl | |
| import gc | |
| import hashlib | |
| import json | |
| import math | |
| import os | |
| from pathlib import Path | |
| import random | |
| import shutil | |
| import sys | |
| import time | |
| import traceback | |
| WORK = Path(__file__).resolve().parent.parent | |
| AUDIT = Path('/marimo/rem3di_training/corrected_20261027/evaluation/paper_audit') | |
| ORIGINAL = AUDIT/'author_checkpoint/exp138/polar' | |
| sys.path[:0] = [str(AUDIT),str(AUDIT/'author_legacy_source'),'/marimo/Rem3Di'] | |
| os.environ.update(CUDA_VISIBLE_DEVICES='0',OMP_NUM_THREADS='8',MKL_NUM_THREADS='8',WANDB_MODE='online') | |
| from dotenv import load_dotenv | |
| load_dotenv('/marimo/.env',override=False) | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| from torch import nn | |
| import yaml | |
| from huggingface_hub import HfApi, hf_hub_download, snapshot_download, CommitOperationAdd | |
| from threedscriptors.configuration.architecture_config import EncoderOnlyArchitectureConfig | |
| from threedscriptors.data_handling.sample import Sample as LegacySample | |
| from remedi.data_handling.sample import Sample, yield_molecules_collate_fn | |
| from build_author_mace_runtime import build_author_mace_runtime | |
| torch.serialization.add_safe_globals([slice]) | |
| torch.set_num_threads(8) | |
| def write(path,value): | |
| path.parent.mkdir(parents=True,exist_ok=True) | |
| tmp=path.with_suffix(path.suffix+'.partial') | |
| tmp.write_text(json.dumps(value,indent=2,allow_nan=False));tmp.replace(path) | |
| def sha(path): | |
| h=hashlib.sha256() | |
| with path.open('rb') as f: | |
| for b in iter(lambda:f.read(8<<20),b''):h.update(b) | |
| return h.hexdigest() | |
| def status(phase,**kw): | |
| row=dict(phase=phase,pid=os.getpid(),updated_unix=time.time(),**kw) | |
| write(WORK/'training_status.json',row) | |
| print('STATUS',json.dumps(row),flush=True) | |
| def backbone(path): | |
| d=yaml.safe_load((ORIGINAL/'post_training_architecture_config.yaml').read_text()) | |
| d.pop('decoder_config',None);d['kind']='encoder_only';d['mace_config']=None | |
| cfg=EncoderOnlyArchitectureConfig.model_validate(d) | |
| pre,enc=cfg._build_preprocessor(),cfg._build_encoder() | |
| # Preserve the learned chiral weights in the checkpoint's original precision. | |
| pre.atomic_preprocessor.double();pre.geometric_preprocessor.float();enc.float() | |
| for name,module in [('encoder.pth',enc),('atomic_preprocessor.pth',pre.atomic_preprocessor),('geometric_preprocessor.pth',pre.geometric_preprocessor)]: | |
| module.load_state_dict(torch.load(path/name,map_location='cpu',weights_only=True),strict=True) | |
| return pre.cuda(),enc.cuda() | |
| class COSMORegressor(nn.Module): | |
| def __init__(self,pre,enc): | |
| super().__init__();self.pre=pre;self.encoder=enc | |
| self.head=nn.Sequential(nn.Linear(640,256),nn.SiLU(),nn.Linear(256,155)).cuda() | |
| def forward(self,sample): | |
| z=self.encoder(self.pre(sample)) | |
| if not torch.is_tensor(z):z=z.flat | |
| return self.head(z) | |
| def batches(indices,lengths,rng=None,max_size=96,atom_cap=4000): | |
| order=np.asarray(indices).copy() | |
| if rng is not None:rng.shuffle(order) | |
| for start in range(0,len(order),1024): | |
| chunk=sorted(order[start:start+1024],key=lambda i:lengths[i]);ids=[];atoms=0 | |
| for i in chunk: | |
| if ids and (atoms+lengths[i]>atom_cap or len(ids)>=max_size): | |
| yield ids;ids=[];atoms=0 | |
| ids.append(int(i));atoms+=lengths[i] | |
| if ids:yield ids | |
| def inverse(z,mean,scale): | |
| t=z*scale+mean | |
| # Clamp only the metrics' physical reconstruction to the valid range. | |
| return torch.cat([torch.expm1(t[:,:153]).clamp_min(0),torch.exp(t[:,153:])],dim=1) | |
| def loss_terms(pred,target): | |
| e=(pred-target).square() | |
| bins=e[:,:153].mean();area=e[:,153].mean();volume=e[:,154].mean() | |
| return (bins+area+volume)/3,dict(sigma=float(bins.detach()),area=float(area.detach()),volume=float(volume.detach())) | |
| def upload_epoch(api,cfg,path): | |
| marker=path/'hub_verified.json' | |
| if marker.exists():return json.loads(marker.read_text()) | |
| names=['encoder.pth','atomic_preprocessor.pth','geometric_preprocessor.pth','cosmo_head.pth', | |
| 'training_state.pt','metrics.json','manifest.json','label_transform.json','post_training_architecture_config.yaml','cosmo_decoder_config.json'] | |
| ops=[CommitOperationAdd(path_in_repo=f'epoch_{path.name.split("_")[-1]}/{n}',path_or_fileobj=str(path/n)) for n in names] | |
| commit=api.create_commit(repo_id=cfg['model_repository'],operations=ops, | |
| commit_message=f'Save complete COSMO epoch {path.name}: weights, optimizer, scheduler, RNG and transform') | |
| entries={s.rfilename:s for s in api.model_info(cfg['model_repository'],revision=commit.oid,files_metadata=True).siblings} | |
| for n in ['encoder.pth','atomic_preprocessor.pth','geometric_preprocessor.pth','cosmo_head.pth','training_state.pt']: | |
| entry=entries[f'{path.name}/{n}'] | |
| remote=entry.lfs.sha256 if entry.lfs else None | |
| assert remote==sha(path/n),(n,remote) | |
| result=dict(epoch=int(path.name.split('_')[-1]),repository=cfg['model_repository'],revision=commit.oid,verified=True,time=time.time()) | |
| write(marker,result) | |
| return result | |
| def main(): | |
| parser=argparse.ArgumentParser();parser.add_argument('--smoke',action='store_true') | |
| parser.add_argument('--verify-resume',action='store_true');args=parser.parse_args() | |
| lock=(WORK/'training.lock').open('a') | |
| try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB) | |
| except BlockingIOError: | |
| print('TRAINING_ALREADY_RUNNING',flush=True);return | |
| cfg=json.loads((WORK/'config.json').read_text()) | |
| api=HfApi() | |
| status('loading_published_dataset') | |
| reference_path=WORK/'dataset_reference.json' | |
| reference=json.loads(reference_path.read_text()) if reference_path.exists() else {} | |
| revision=reference.get('revision') or api.dataset_info(cfg['dataset_repository']).sha | |
| data_root=Path(snapshot_download(cfg['dataset_repository'],repo_type='dataset',revision=revision, | |
| allow_patterns=['data/*','manifest.json','label_transform.json','pm6_source.json'])) | |
| manifest=json.loads((data_root/'manifest.json').read_text()) | |
| assert manifest['rows']==361583 | |
| # Check every downloaded training shard against the independently assembled manifest. | |
| for name,digest in manifest['files'].items():assert sha(data_root/name)==digest,name | |
| transform=json.loads((data_root/'label_transform.json').read_text()) | |
| frames=[] | |
| for split in ['train','validation']: | |
| frame=pd.concat([pd.read_parquet(p) for p in sorted((data_root/'data').glob(split+'-*.parquet'))],ignore_index=True) | |
| assert len(frame)==manifest['splits'][split] | |
| frame['is_validation']=split=='validation';frames.append(frame) | |
| df=pd.concat(frames,ignore_index=True) | |
| tr=np.flatnonzero(~df.is_validation.values);va=np.flatnonzero(df.is_validation.values) | |
| assert not set(df.molecule_group.iloc[tr])&set(df.molecule_group.iloc[va]) | |
| target_cols=[f'p_{i:03d}' for i in range(153)]+['area_A2','volume_A3'] | |
| labels=df[target_cols].to_numpy(np.float64) | |
| transformed=np.concatenate([np.log1p(labels[:,:153]),np.log(labels[:,153:])],axis=1) | |
| mean=np.asarray(transform['mean']);scale=np.asarray(transform['scale']) | |
| assert np.allclose(transformed[tr].mean(0),mean,atol=1e-10) | |
| assert np.allclose(np.maximum(transformed[tr].std(0),1e-3),scale,atol=1e-10) | |
| targets=torch.tensor((transformed-mean)/scale,dtype=torch.float32,device='cuda') | |
| mean=torch.tensor(mean,dtype=torch.float32,device='cuda');scale=torch.tensor(scale,dtype=torch.float32,device='cuda') | |
| positions=[torch.tensor(np.stack(x),dtype=torch.float32) for x in df.positions] | |
| numbers=[torch.tensor(np.asarray(x),dtype=torch.long) for x in df.atomic_numbers] | |
| lengths=np.array([len(z) for z in numbers]) | |
| assert np.isfinite(labels).all() | |
| write(WORK/'dataset_reference.json',dict(repository=cfg['dataset_repository'],revision=revision,manifest=manifest)) | |
| completed=sorted((WORK/'checkpoints').glob('epoch_[0-9][0-9]')) if (WORK/'checkpoints').exists() else [] | |
| if completed and not args.smoke: | |
| init=completed[-1] | |
| else: | |
| init=WORK/'initial_epoch_02';init.mkdir(exist_ok=True) | |
| for name in ['encoder.pth','atomic_preprocessor.pth','geometric_preprocessor.pth','manifest.json']: | |
| src=Path(hf_hub_download(cfg['initial_checkpoint_repository'], | |
| filename=cfg['initial_checkpoint_path']+'/'+name,revision=cfg['initial_checkpoint_revision'])) | |
| if not (init/name).exists():shutil.copyfile(src,init/name) | |
| initial_manifest=json.loads((init/'manifest.json').read_text()) | |
| for name in ['encoder.pth','atomic_preprocessor.pth','geometric_preprocessor.pth']: | |
| assert sha(init/name)==initial_manifest['files'][name]['sha256'] | |
| torch.manual_seed(cfg['seed']);torch.cuda.manual_seed_all(cfg['seed']);np.random.seed(cfg['seed']);random.seed(cfg['seed']) | |
| pre,enc=backbone(init) | |
| model=COSMORegressor(pre,enc) | |
| optimizer=torch.optim.AdamW([ | |
| {'params':list(pre.parameters())+list(enc.parameters()),'lr':cfg['learning_rate_backbone']}, | |
| {'params':model.head.parameters(),'lr':cfg['learning_rate_head']}],weight_decay=cfg['weight_decay']) | |
| # Epoch schedule has a stable horizon even when atom-budget packing varies. | |
| scheduler=torch.optim.lr_scheduler.LambdaLR(optimizer,lambda e:.1+.9*.5*(1+math.cos(math.pi*e/cfg['epochs']))) | |
| mace=build_author_mace_runtime().cuda().eval().requires_grad_(False) | |
| captures=[] | |
| hook=mace.preprocessor.atomic_preprocessor.register_forward_pre_hook(lambda m,a:captures.append(a[0].detach())) | |
| frozen_buffers={n:b.detach().cpu().clone() for n,b in pre.atomic_preprocessor.invariant_normalization.named_buffers()} | |
| def sample(ids): | |
| batch=yield_molecules_collate_fn([Sample(atomic_positions=positions[i],atomic_numbers=numbers[i],total_charge=0.,multiplicity=1.) for i in ids]).to_('cuda') | |
| captures.clear() | |
| with torch.no_grad():mace.preprocessor(batch) | |
| assert len(captures)==1 | |
| raw=captures.pop().clone() | |
| return LegacySample(embeddings=raw,atomic_positions=batch.atomic_positions,padding_mask=batch.padding_mask) | |
| def update(ids): | |
| optimizer.zero_grad(set_to_none=True) | |
| pred=model(sample(ids));loss,parts=loss_terms(pred,targets[ids]) | |
| assert torch.isfinite(loss),'Nonfinite COSMO loss' | |
| loss.backward() | |
| norm=nn.utils.clip_grad_norm_(model.parameters(),1.,error_if_nonfinite=True) | |
| optimizer.step() | |
| return float(loss.detach()),parts,float(norm) | |
| if args.smoke: | |
| ids=tr[:8].tolist();before=next(enc.parameters()).detach().clone() | |
| model.train();result=update(ids) | |
| assert not torch.equal(before,next(enc.parameters()).detach()),'Encoder did not update' | |
| assert any(p.grad is not None and bool((p.grad!=0).any()) for p in pre.parameters()) | |
| assert all(p.grad is None for p in mace.parameters()) | |
| model.eval() | |
| with torch.no_grad(): | |
| s=sample(ids);ref=model(s);physical=inverse(targets[ids],mean,scale) | |
| assert np.allclose(physical.cpu().numpy(),labels[ids],rtol=1e-5,atol=1e-4) | |
| state={k:v.cpu() for k,v in model.state_dict().items()} | |
| path=WORK/'smoke_model.pt';torch.save(state,path) | |
| model.load_state_dict(torch.load(path,map_location='cpu',weights_only=True)) | |
| with torch.no_grad():assert torch.allclose(ref,model(s),atol=1e-6,rtol=1e-6) | |
| for n,b in pre.atomic_preprocessor.invariant_normalization.named_buffers():assert torch.equal(b.cpu(),frozen_buffers[n]) | |
| write(WORK/'smoke_validation.json',dict(passed=True,loss=result[0],gradient_norm=result[2], | |
| decoder_outputs=155,encoder_updated=True,preprocessor_gradients=True,mace_frozen=True, | |
| transform_roundtrip=True,checkpoint_roundtrip=True,dataset_revision=revision)) | |
| path.unlink();status('smoke_passed');return | |
| smoke=json.loads((WORK/'smoke_validation.json').read_text()) | |
| assert smoke['passed'] and smoke['dataset_revision']==revision,'Smoke validation must match the pinned dataset' | |
| import wandb | |
| api.create_repo(cfg['model_repository'],repo_type='model',exist_ok=True) | |
| wb=json.loads((WORK/'wandb.json').read_text()) if (WORK/'wandb.json').exists() else {'run_id':wandb.util.generate_id()} | |
| write(WORK/'wandb.json',wb) | |
| run=None | |
| if not args.verify_resume: | |
| run=wandb.init(entity=cfg['wandb_entity'],project=cfg['wandb_project'],id=wb['run_id'],resume='allow',mode='online', | |
| name='rem3di-pcqm-epoch2-cosmo-10epochs',group='rem3di-cosmo',job_type='cosmo-posttraining', | |
| config={**cfg,'dataset_revision':revision,'loss':'(mean sigma z-score MSE + A z-score MSE + V z-score MSE)/3', | |
| 'train_rows':len(tr),'validation_rows':len(va),'mace':'frozen; original PM6 coordinates; no geometry augmentation'}, | |
| dir=str(WORK),settings=wandb.Settings(init_timeout=90)) | |
| write(WORK/'wandb.json',dict(run_id=run.id,url=run.url)) | |
| run.define_metric('global_step');run.define_metric('train/*',step_metric='global_step') | |
| run.define_metric('epoch');run.define_metric('validation/*',step_metric='epoch') | |
| start_epoch=1;global_step=0 | |
| if completed: | |
| last=completed[-1] | |
| saved_manifest=json.loads((last/'manifest.json').read_text()) | |
| for name in ['encoder.pth','atomic_preprocessor.pth','geometric_preprocessor.pth','cosmo_head.pth','training_state.pt']: | |
| assert sha(last/name)==saved_manifest['files'][name]['sha256'],name | |
| for n,module in [('encoder.pth',enc),('atomic_preprocessor.pth',pre.atomic_preprocessor),('geometric_preprocessor.pth',pre.geometric_preprocessor),('cosmo_head.pth',model.head)]: | |
| module.load_state_dict(torch.load(last/n,map_location='cpu',weights_only=True)) | |
| state=torch.load(last/'training_state.pt',map_location='cpu',weights_only=False) | |
| assert state['dataset_revision']==revision,'Resume must preserve the original dataset revision' | |
| assert state['config']==cfg,'Resume configuration differs from the saved run' | |
| assert state['epoch']==int(last.name.split('_')[-1]) | |
| optimizer.load_state_dict(state['optimizer']);scheduler.load_state_dict(state['scheduler']) | |
| torch.set_rng_state(state['torch_rng']);torch.cuda.set_rng_state_all(state['cuda_rng']) | |
| np.random.set_state(state['numpy_rng']);random.setstate(state['python_rng']) | |
| start_epoch=state['epoch']+1;global_step=state['global_step'] | |
| upload_epoch(api,cfg,last) | |
| write(WORK/'resume_state.json',dict(restored_epoch=state['epoch'],next_epoch=start_epoch, | |
| global_step=global_step,dataset_revision=revision,optimizer_restored=True,scheduler_restored=True,rng_restored=True)) | |
| print('RESUMED',json.dumps(dict(epoch=state['epoch'],next_epoch=start_epoch,global_step=global_step)),flush=True) | |
| if args.verify_resume: | |
| assert completed,'No completed checkpoint to verify' | |
| model.eval() | |
| with torch.no_grad(): | |
| output=model(sample(tr[:8].tolist())) | |
| loss,_=loss_terms(output,targets[tr[:8]]) | |
| physical=inverse(output,mean,scale) | |
| assert output.shape==(8,155) and torch.isfinite(output).all() and torch.isfinite(physical).all() | |
| assert torch.isfinite(loss) and optimizer.state and scheduler.last_epoch==start_epoch-1 | |
| write(WORK/'resume_validation.json',dict(passed=True,restored_epoch=start_epoch-1,next_epoch=start_epoch, | |
| global_step=global_step,loss=float(loss),optimizer_state_tensors=len(optimizer.state), | |
| scheduler_epoch=scheduler.last_epoch,dataset_revision=revision,finite_predictions=True)) | |
| hook.remove();status('resume_verified',next_epoch=start_epoch,global_step=global_step);return | |
| try: | |
| for epoch in range(start_epoch,cfg['epochs']+1): | |
| model.train();started=time.time();loss_sum=0.;count=0 | |
| rng=np.random.default_rng(cfg['seed']+epoch) | |
| plan=list(batches(tr,lengths,rng,max_size=cfg['max_molecules_per_batch'],atom_cap=cfg['max_atoms_per_batch'])) | |
| status('training',epoch=epoch,epochs=cfg['epochs'],batches=len(plan),wandb_url=run.url) | |
| for bi,ids in enumerate(plan,1): | |
| loss,parts,norm=update(ids) | |
| global_step+=1;loss_sum+=loss*len(ids);count+=len(ids) | |
| if bi==1 or bi%25==0: | |
| row=dict(global_step=global_step,epoch=epoch,batch=bi,batches=len(plan), | |
| **{'train/'+k:v for k,v in dict(loss=loss,gradient_norm=norm,backbone_lr=optimizer.param_groups[0]['lr'],head_lr=optimizer.param_groups[1]['lr'],**parts).items()}) | |
| run.log(row) | |
| status('training',**row,wandb_url=run.url,molecules_seen=count,seconds=time.time()-started) | |
| model.eval();val_sum=0.;val_count=0;sse=np.zeros(3);mae=np.zeros(3);sumarea=0. | |
| with torch.no_grad(): | |
| for ids in batches(va,lengths,max_size=cfg['max_molecules_per_batch'],atom_cap=cfg['max_atoms_per_batch']): | |
| pred=model(sample(ids));loss,_=loss_terms(pred,targets[ids]) | |
| val_sum+=float(loss)*len(ids);val_count+=len(ids) | |
| physical=inverse(pred,mean,scale).cpu().numpy() | |
| assert np.isfinite(physical).all() | |
| err=physical-labels[ids] | |
| sse += [float((err[:,:153]**2).sum()/153),float((err[:,153]**2).sum()),float((err[:,154]**2).sum())] | |
| mae += [float(abs(err[:,:153]).sum()/153),float(abs(err[:,153]).sum()),float(abs(err[:,154]).sum())] | |
| sumarea+=float(abs(physical[:,:153].sum(1)-physical[:,153]).sum()) | |
| assert count==len(tr) and val_count==len(va) | |
| for n,b in pre.atomic_preprocessor.invariant_normalization.named_buffers():assert torch.equal(b.cpu(),frozen_buffers[n]) | |
| scheduler.step() | |
| metrics=dict(epoch=epoch,global_step=global_step,train_loss=loss_sum/count,validation_loss=val_sum/val_count, | |
| validation_sigma_rmse=float(np.sqrt(sse[0]/val_count)),validation_area_rmse=float(np.sqrt(sse[1]/val_count)), | |
| validation_volume_rmse=float(np.sqrt(sse[2]/val_count)),validation_sigma_mae=float(mae[0]/val_count), | |
| validation_area_mae=float(mae[1]/val_count),validation_volume_mae=float(mae[2]/val_count), | |
| validation_profile_area_inconsistency=float(sumarea/val_count),seconds=time.time()-started) | |
| assert all(math.isfinite(v) for v in metrics.values()) | |
| target=WORK/'checkpoints'/f'epoch_{epoch:02d}' | |
| pending=target.with_name(target.name+'.partial');pending.mkdir(parents=True,exist_ok=True) | |
| for n,module in [('encoder.pth',enc),('atomic_preprocessor.pth',pre.atomic_preprocessor),('geometric_preprocessor.pth',pre.geometric_preprocessor),('cosmo_head.pth',model.head)]: | |
| torch.save(module.state_dict(),pending/n) | |
| torch.save(dict(epoch=epoch,global_step=global_step,optimizer=optimizer.state_dict(),scheduler=scheduler.state_dict(), | |
| torch_rng=torch.get_rng_state(),cuda_rng=torch.cuda.get_rng_state_all(),numpy_rng=np.random.get_state(), | |
| python_rng=random.getstate(),dataset_revision=revision,config=cfg),pending/'training_state.pt') | |
| write(pending/'metrics.json',metrics) | |
| shutil.copyfile(data_root/'label_transform.json',pending/'label_transform.json') | |
| architecture=yaml.safe_load((ORIGINAL/'post_training_architecture_config.yaml').read_text()) | |
| architecture.pop('decoder_config',None);architecture['kind']='encoder_only' | |
| architecture['mace_config']['model_path']='MACE-POLAR-1-M.model' | |
| (pending/'post_training_architecture_config.yaml').write_text(yaml.safe_dump(architecture,sort_keys=False)) | |
| write(pending/'cosmo_decoder_config.json',dict(input_features=640,hidden_features=256,output_features=155,activation='SiLU')) | |
| write(pending/'manifest.json',dict(epoch=epoch,dataset_revision=revision,config=cfg, | |
| files={p.name:dict(bytes=p.stat().st_size,sha256=sha(p)) for p in pending.iterdir() if p.is_file()})) | |
| pending.rename(target) | |
| status('uploading_checkpoint',epoch=epoch,metrics=metrics,wandb_url=run.url) | |
| for attempt in range(5): | |
| try: | |
| result=upload_epoch(api,cfg,target);break | |
| except Exception: | |
| if attempt==4:raise | |
| print('UPLOAD_RETRY',traceback.format_exc(),flush=True);time.sleep(min(10*2**attempt,60)) | |
| run.log(dict(epoch=epoch,**{'validation/'+k.removeprefix('validation_'):v for k,v in metrics.items() if k.startswith('validation_')}, | |
| **{'epoch/'+k:v for k,v in metrics.items()},**{'backup/revision':result['revision'],'backup/verified_epoch':epoch})) | |
| write(WORK/'latest_epoch.json',dict(**metrics,hub=result,wandb_url=run.url)) | |
| write(WORK/'training_complete.json',dict(epochs=cfg['epochs'],wandb_url=run.url,completed_unix=time.time())) | |
| run.summary['training_complete']=True;run.finish();status('complete',epochs=cfg['epochs']) | |
| except Exception: | |
| status('failed',error=traceback.format_exc());run.finish(exit_code=1);raise | |
| finally: | |
| hook.remove() | |
| if __name__=='__main__': | |
| try:main() | |
| except Exception: | |
| status('failed',error=traceback.format_exc());raise | |