File size: 5,701 Bytes
98bde72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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()