PepPA / src /peppa /native_worker.py
pranamanam's picture
Upload 97 files
98bde72 verified
Raw
History Blame Contribute Delete
5.7 kB
"""JSON worker for pinned moPPIt and PeptiVerse installations.
Run inside the scientific model's own environment with this package installed.
Artifacts are written to a persistent work directory supplied by the operator.
"""
import argparse,csv,hashlib,importlib.util,json,os,subprocess,sys
from pathlib import Path
from .schema import Candidate,Molecule,ToolResult,canonical
def run(mode,root,work,request,output):
root=Path(root).resolve();work=Path(work).resolve();work.mkdir(parents=True,exist_ok=True)
d=json.loads(Path(request).read_text());a=d['arguments'];state=d['state']
if mode=='moppit':
length=int(a.get('length',12));count=int(a.get('count',32))
if length not in state['spec']['sequence_lengths'] or not 1<=count<=96:raise ValueError('generation limits')
objectives=a.get('objectives',['Affinity','Solubility'])
if set(objectives)-{'Affinity','Solubility','Hemolysis','Non-Fouling','Permeability','Half-Life','Motif'}:raise ValueError('unsupported native objective')
weights=a.get('weights',[1/len(objectives)]*len(objectives))
if len(weights)!=len(objectives) or any(float(x)<0 for x in weights) or sum(weights)<=0:raise ValueError('invalid weights')
native_order=['Hemolysis','Non-Fouling','Solubility','Permeability','Half-Life','Affinity','Motif']
weight_map=dict(zip(objectives,weights));objectives=[x for x in native_order if x in weight_map];weights=[weight_map[x] for x in objectives]
if int(a.get('steps',100))!=100:raise ValueError('pinned native entrypoint fixes 100 integration steps')
if state['spec']['peptide_format']!='linear':raise ValueError('this worker implements canonical linear moPPIt proposals')
target=state['spec']['targets'][0]['molecule']['sequence']
job=hashlib.sha256(canonical(d).encode()).hexdigest()[:16];csvpath=work/(job+'.csv')
if csvpath.exists():raise ValueError('output exists; use a new episode or explicit cached result')
argv=[sys.executable,'moppit.py','--target_protein',target,'--length',str(length),'--n_samples','1','--n_batches',str(count),
'--T',str(int(a.get('steps',100))),'--objectives',*objectives,'--weights',*[str(float(x)) for x in weights],'--output_file',str(csvpath)]
if 'Motif' in objectives:argv+=['--motifs',str(a['native_motif_indices'])]
# Set all relevant random seeds before executing the native CLI.
bootstrap='import runpy,sys,random,numpy as np,torch; s=int(sys.argv[1]); random.seed(s); np.random.seed(s); torch.manual_seed(s); torch.cuda.manual_seed_all(s); sys.argv=sys.argv[2:]; runpy.run_path(sys.argv[0],run_name="__main__")'
seed=int(state['spec']['seed']);argv=[sys.executable,'-c',bootstrap,str(seed),*argv[1:]]
with (work/(job+'.log')).open('w') as log:
subprocess.run(argv,cwd=root,stdout=log,stderr=subprocess.STDOUT,check=True,timeout=7200)
with csvpath.open() as f:rows=list(csv.DictReader(f))
if not rows:raise ValueError('native generator returned no candidates')
candidates=[Candidate(molecule=Molecule(sequence=r['Binder'].strip()),generator='ChatterjeeLab/moPPIt',revision='29de994bfb6c67890d0efca25d1cf431eea96999') for r in rows]
result=ToolResult(candidates=candidates,artifacts={'native_scores':str(csvpath)},message='Native scores retained in CSV with original definitions; attach calibration before conservative ranking.')
elif mode=='peptiverse':
sys.path.insert(0,str(root));os.chdir(root)
module_spec=importlib.util.spec_from_file_location('peppaverse_native',root/'inference.py');module=importlib.util.module_from_spec(module_spec);module_spec.loader.exec_module(module)
predictor=module.PeptiVersePredictor(manifest_path=str(root/'best_models.txt'),classifier_weight_root=str(root),device=a.get('device','cuda'))
raw={};target=state['spec']['targets'][0]['molecule']['sequence']
for cid in a['candidate_ids']:
mol=Molecule.model_validate(state['candidates'][cid]['molecule'])
if mol.modifications or mol.bonds or mol.n_terminus!='free' or mol.c_terminus!='free':raise ValueError('use a chemistry-compatible native model for modified candidates')
properties=a.get('properties',['solubility','hemolysis'])
allowed={'solubility','hemolysis','nf','permeability_penetrance','halflife','affinity'}
if set(properties)-allowed:raise ValueError('unsupported sequence endpoint')
raw[cid]={}
for prop in properties:
value=predictor.predict_binding_affinity(col='wt',target_seq=target,binder_str=mol.sequence) if prop=='affinity' else predictor.predict_property(prop,col='wt',input_str=mol.sequence)
raw[cid][prop]=value
job=hashlib.sha256(canonical(d).encode()).hexdigest()[:16];p=work/(job+'_raw.json')
def serial(x):
if hasattr(x,'tolist'):return x.tolist()
if hasattr(x,'item'):return x.item()
raise TypeError(type(x).__name__)
p.write_text(json.dumps(raw,default=serial,indent=2,allow_nan=False))
result=ToolResult(artifacts={'peptiverse_native':str(p)},message='Native predictions preserved. Use endpoint-specific calibration before ranking.')
else:raise ValueError(mode)
Path(output).write_text(result.model_dump_json(indent=2))
def main():
p=argparse.ArgumentParser();p.add_argument('--mode',choices=['moppit','peptiverse'],required=True);p.add_argument('--root',required=True);p.add_argument('--work',required=True);p.add_argument('request');p.add_argument('output');run(**vars(p.parse_args()))
if __name__=='__main__':main()