abdullahsamra commited on
Commit
89d9c28
·
0 Parent(s):

Deploy AILIXIR generation API Docker Space

Browse files
Files changed (44) hide show
  1. .dockerignore +18 -0
  2. .gitattributes +4 -0
  3. .gitignore +17 -0
  4. Dockerfile +142 -0
  5. README.md +19 -0
  6. api.py +675 -0
  7. bundle/.gitattributes +62 -0
  8. bundle/README.md +18 -0
  9. bundle/configs/backend_contract.json +80 -0
  10. bundle/configs/sampling_model.bundle.toml +16 -0
  11. bundle/docking/maps_current/4WKQ_clean.pdb +0 -0
  12. bundle/docking/maps_current/4WKQ_raw.pdb +0 -0
  13. bundle/docking/maps_current/4WKQ_receptor.A.map +3 -0
  14. bundle/docking/maps_current/4WKQ_receptor.Br.map +3 -0
  15. bundle/docking/maps_current/4WKQ_receptor.C.map +3 -0
  16. bundle/docking/maps_current/4WKQ_receptor.Cl.map +3 -0
  17. bundle/docking/maps_current/4WKQ_receptor.F.map +3 -0
  18. bundle/docking/maps_current/4WKQ_receptor.HD.map +3 -0
  19. bundle/docking/maps_current/4WKQ_receptor.N.map +3 -0
  20. bundle/docking/maps_current/4WKQ_receptor.NA.map +3 -0
  21. bundle/docking/maps_current/4WKQ_receptor.OA.map +3 -0
  22. bundle/docking/maps_current/4WKQ_receptor.S.map +3 -0
  23. bundle/docking/maps_current/4WKQ_receptor.SA.map +3 -0
  24. bundle/docking/maps_current/4WKQ_receptor.d.map +3 -0
  25. bundle/docking/maps_current/4WKQ_receptor.e.map +3 -0
  26. bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.fld +52 -0
  27. bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.xyz +3 -0
  28. bundle/docking/maps_current/autogrid_v9.log +0 -0
  29. bundle/docking/maps_current/ligand.sdf +217 -0
  30. bundle/docking/maps_current/receptorH.gpf +22 -0
  31. bundle/docking/maps_current/receptorH_flex.pdbqt +13 -0
  32. bundle/docking/maps_current/receptorH_rigid.pdbqt +0 -0
  33. bundle/docs/API_RESPONSE_SPEC.md +111 -0
  34. bundle/examples/generate_response.example.json +131 -0
  35. bundle/models/affinity/config.pkl +3 -0
  36. bundle/models/affinity/model.pt +3 -0
  37. bundle/models/affinity/target_sequence.txt +1 -0
  38. bundle/models/generator/egfr_generator.chkpt +3 -0
  39. bundle/models/generator/reinvent.prior +3 -0
  40. bundle/services/deeppurpose/serve_affinity.py +187 -0
  41. bundle/tools/dock_enriched.py +204 -0
  42. bundle/tools/enrich_generated.py +191 -0
  43. bundle_files.txt +0 -0
  44. start_hf.sh +63 -0
.dockerignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ outputs
2
+ .venv
3
+ .git
4
+ __pycache__
5
+ .pytest_cache
6
+ .mypy_cache
7
+ .cache
8
+ bin
9
+
10
+ *.tar
11
+ *.tar.gz
12
+ *.zip
13
+ *.bak
14
+ *.pyc
15
+ test_*.csv
16
+ demo_*.csv
17
+ release_test_*.csv
18
+ generated_results.csv
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.pt filter=lfs diff=lfs merge=lfs -text
2
+ *.chkpt filter=lfs diff=lfs merge=lfs -text
3
+ *.prior filter=lfs diff=lfs merge=lfs -text
4
+ *.map filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ outputs/
2
+ .venv/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .cache/
7
+ bin/
8
+
9
+ *.tar
10
+ *.tar.gz
11
+ *.zip
12
+ *.bak
13
+ *.pyc
14
+ test_*.csv
15
+ demo_*.csv
16
+ release_test_*.csv
17
+ generated_results.csv
Dockerfile ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM condaforge/miniforge3:24.11.3-0
2
+
3
+ WORKDIR /app
4
+
5
+ ENV DEBIAN_FRONTEND=noninteractive
6
+ ENV PATH=/opt/conda/bin:$PATH
7
+ ENV LD_LIBRARY_PATH=/opt/conda/envs/ailixir/lib:/opt/conda/envs/dp/lib:/opt/conda/lib:/usr/local/lib:/usr/lib/x86_64-linux-gnu
8
+
9
+ SHELL ["/bin/bash", "-lc"]
10
+
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ git \
13
+ wget \
14
+ curl \
15
+ ca-certificates \
16
+ build-essential \
17
+ make \
18
+ procps \
19
+ ocl-icd-opencl-dev \
20
+ pocl-opencl-icd \
21
+ clinfo \
22
+ && rm -rf /var/lib/apt/lists/*
23
+
24
+ # -----------------------------
25
+ # Generation / REINVENT env
26
+ # -----------------------------
27
+ RUN conda create -n ailixir python=3.10 -y && conda clean -afy
28
+
29
+ SHELL ["conda", "run", "--no-capture-output", "-n", "ailixir", "/bin/bash", "-lc"]
30
+
31
+ RUN set -eux; \
32
+ for i in 1 2 3 4 5; do \
33
+ if conda install -y -c conda-forge \
34
+ pandas \
35
+ numpy \
36
+ scikit-learn \
37
+ rdkit \
38
+ fastapi \
39
+ uvicorn \
40
+ requests \
41
+ tqdm \
42
+ pip; then \
43
+ conda clean -afy; \
44
+ exit 0; \
45
+ fi; \
46
+ echo "conda install failed on attempt ${i}, retrying..."; \
47
+ conda clean -i -y || true; \
48
+ sleep $((i * 20)); \
49
+ done; \
50
+ echo "conda install failed after retries"; \
51
+ exit 1
52
+
53
+ RUN git clone --depth 1 https://github.com/MolecularAI/REINVENT4.git /opt/REINVENT4 \
54
+ && cd /opt/REINVENT4 \
55
+ && python install.py cpu
56
+
57
+ RUN python -c "from rdkit import Chem; print('RDKit import OK')"
58
+ RUN python -c "import importlib.metadata as m; print('REINVENT version', m.version('reinvent'))"
59
+
60
+ # -----------------------------
61
+ # DeepPurpose affinity env
62
+ # -----------------------------
63
+ SHELL ["/bin/bash", "-lc"]
64
+
65
+ RUN conda create -n dp python=3.8 -y && conda clean -afy
66
+
67
+ SHELL ["conda", "run", "--no-capture-output", "-n", "dp", "/bin/bash", "-lc"]
68
+
69
+ RUN set -eux; \
70
+ for i in 1 2 3 4 5; do \
71
+ if conda install -y -c conda-forge \
72
+ pandas \
73
+ numpy \
74
+ scikit-learn \
75
+ rdkit \
76
+ fastapi \
77
+ uvicorn \
78
+ requests \
79
+ tqdm \
80
+ pip; then \
81
+ conda clean -afy; \
82
+ exit 0; \
83
+ fi; \
84
+ echo "conda install failed on attempt ${i}, retrying..."; \
85
+ conda clean -i -y || true; \
86
+ sleep $((i * 20)); \
87
+ done; \
88
+ echo "conda install failed after retries"; \
89
+ exit 1
90
+
91
+ ENV PIP_DEFAULT_TIMEOUT=1000
92
+ ENV PIP_RETRIES=10
93
+ ENV PIP_DISABLE_PIP_VERSION_CHECK=1
94
+
95
+ RUN pip install --no-cache-dir --default-timeout=1000 --retries=10 \
96
+ git+https://github.com/bp-kelley/descriptastorus
97
+
98
+ RUN pip install --no-cache-dir --default-timeout=1000 --retries=10 \
99
+ torch==2.1.0 \
100
+ --index-url https://download.pytorch.org/whl/cpu
101
+
102
+ RUN python -c "import torch; print('Torch version:', torch.__version__); print('CUDA available:', torch.cuda.is_available())"
103
+
104
+ RUN pip install --no-cache-dir --default-timeout=1000 --retries=10 \
105
+ DeepPurpose
106
+
107
+ # -----------------------------
108
+ # AutoDock-GPU CPU/OpenCL build
109
+ # -----------------------------
110
+ SHELL ["/bin/bash", "-lc"]
111
+
112
+ RUN set -eux; \
113
+ clinfo || true; \
114
+ rm -rf /opt/AutoDock-GPU; \
115
+ git clone --depth 1 https://github.com/ccsb-scripps/AutoDock-GPU.git /opt/AutoDock-GPU; \
116
+ cd /opt/AutoDock-GPU; \
117
+ make DEVICE=CPU NUMWI=1 GPU_INCLUDE_PATH=/usr/include GPU_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu; \
118
+ find /opt/AutoDock-GPU -maxdepth 3 -type f -name "autodock_*" -print; \
119
+ test -f /opt/AutoDock-GPU/bin/autodock_cpu_1wi; \
120
+ chmod +x /opt/AutoDock-GPU/bin/autodock_cpu_1wi
121
+
122
+ COPY api.py /app/api.py
123
+ COPY bundle /app/bundle
124
+ COPY start_hf.sh /app/start_hf.sh
125
+
126
+ RUN sed -i 's/\r$//' /app/start_hf.sh \
127
+ && chmod +x /app/start_hf.sh \
128
+ && mkdir -p /app/outputs/jobs /app/outputs/deeppurpose \
129
+ && mkdir -p /home/abdullah/projects/egfr_drug_discovery/runs/deeppurpose
130
+
131
+
132
+ ENV PUBLIC_BASE_URL=http://localhost:7860
133
+ ENV REINVENT_DEVICE=cpu
134
+ ENV DEEPPURPOSE_URL=http://127.0.0.1:8001/reinvent_predict
135
+ ENV ADGPU_BIN=/opt/AutoDock-GPU/bin/autodock_cpu_1wi
136
+ ENV PYTHON=python
137
+ ENV LD_PRELOAD=/opt/conda/envs/ailixir/lib/libstdc++.so.6
138
+ ENV PYTHONPATH=/app/bundle
139
+
140
+ EXPOSE 7860
141
+
142
+ CMD ["/app/start_hf.sh"]
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AILIXIR EGFR Generation API
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # AILIXIR EGFR Generation API
12
+
13
+ FastAPI service for EGFR molecule generation, DeepPurpose affinity scoring, and optional CPU docking.
14
+
15
+ Recommended defaults for Hugging Face free CPU:
16
+ - docking_mode: off
17
+ - dock_top_k: 1
18
+
19
+ CPU docking is supported but slow.
api.py ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import uuid
5
+ import shutil
6
+ import subprocess
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Literal, Optional, List, Dict, Any
10
+
11
+ import pandas as pd
12
+ from fastapi import FastAPI, HTTPException, BackgroundTasks, Request
13
+ from fastapi.responses import FileResponse, JSONResponse
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from pydantic import BaseModel, Field
16
+
17
+ from rdkit import Chem
18
+ from rdkit.Chem import AllChem
19
+
20
+
21
+ APP_ROOT = Path(__file__).resolve().parent
22
+ BUNDLE_DIR = APP_ROOT / "bundle"
23
+ OUTPUTS_DIR = APP_ROOT / "outputs"
24
+ JOBS_DIR = OUTPUTS_DIR / "jobs"
25
+
26
+ PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "http://localhost:8000").rstrip("/")
27
+ DEEPPURPOSE_URL = os.getenv("DEEPPURPOSE_URL", "http://127.0.0.1:7860/reinvent_predict")
28
+ REINVENT_DEVICE = os.getenv("REINVENT_DEVICE", "cpu")
29
+ ADGPU_BIN = os.getenv("ADGPU_BIN", "")
30
+
31
+ JOBS_DIR.mkdir(parents=True, exist_ok=True)
32
+
33
+ # Attempt to load DeepPurpose affinity service (optional)
34
+ try:
35
+ from bundle.services.deeppurpose.serve_affinity import predict_smiles_list
36
+ HAS_AFFINITY = True
37
+ except Exception:
38
+ HAS_AFFINITY = False
39
+
40
+
41
+ app = FastAPI(
42
+ title="Ailixir EGFR Pipeline API",
43
+ version="1.0.0",
44
+ description="Production API for EGFR generation, scoring, docking, and ligand export."
45
+ )
46
+
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=os.getenv("CORS_ALLOW_ORIGINS", "*").split(","),
50
+ allow_credentials=False,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+
56
+ # -----------------------------
57
+ # Request models
58
+ # -----------------------------
59
+
60
+ class GenerateRequest(BaseModel):
61
+ preset: str = Field(default="egfr_generator")
62
+ num_molecules: int = Field(default=100, ge=1, le=5000)
63
+ return_top_k: int = Field(default=20, ge=1, le=1000)
64
+ docking_mode: Literal["off", "top_k", "all"] = "off"
65
+ dock_top_k: int = Field(default=10, ge=1, le=1000)
66
+
67
+
68
+ class LigandExportRequest(BaseModel):
69
+ smiles: str
70
+ format: Literal["pdb", "pdbqt", "mol2"]
71
+
72
+
73
+ # -----------------------------
74
+ # Helpers
75
+ # -----------------------------
76
+
77
+ def new_job_id(prefix: str) -> str:
78
+ stamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
79
+ short = uuid.uuid4().hex[:6]
80
+ return f"{prefix}_{stamp}_{short}"
81
+
82
+
83
+ def make_file_meta(job_id: str, file_path: Path) -> Dict[str, str]:
84
+ rel_path = file_path.relative_to(JOBS_DIR / job_id).as_posix()
85
+ relative_url = f"/files/jobs/{job_id}/{rel_path}"
86
+ return {
87
+ "filename": file_path.name,
88
+ "relative_url": relative_url,
89
+ "download_url": f"{PUBLIC_BASE_URL}{relative_url}",
90
+ }
91
+
92
+
93
+ def clean_record(record: Dict[str, Any]) -> Dict[str, Any]:
94
+ """
95
+ Public response cleaner.
96
+ Removes internal paths and confusing technical fields.
97
+ Keeps molecule scores and user-facing values only.
98
+ """
99
+ drop_keys = {
100
+ "docking_pose_file",
101
+ "file_path",
102
+ "internal_path",
103
+ "pose_file",
104
+ }
105
+
106
+ out = {}
107
+ for k, v in record.items():
108
+ if k in drop_keys:
109
+ continue
110
+
111
+ # Convert NaN to None for valid JSON
112
+ try:
113
+ if pd.isna(v):
114
+ out[k] = None
115
+ else:
116
+ out[k] = v
117
+ except Exception:
118
+ out[k] = v
119
+
120
+ return out
121
+
122
+
123
+ def dataframe_to_public_records(df: pd.DataFrame) -> List[Dict[str, Any]]:
124
+ records = []
125
+ for i, row in df.reset_index(drop=True).iterrows():
126
+ item = clean_record(row.to_dict())
127
+ item["rank"] = i + 1
128
+ records.append(item)
129
+ return records
130
+
131
+
132
+ def run_cmd(cmd: List[str], cwd: Optional[Path] = None, timeout: Optional[int] = None):
133
+ result = subprocess.run(
134
+ cmd,
135
+ cwd=str(cwd) if cwd else None,
136
+ text=True,
137
+ stdout=subprocess.PIPE,
138
+ stderr=subprocess.STDOUT,
139
+ timeout=timeout,
140
+ check=False,
141
+ )
142
+ return result
143
+
144
+
145
+ def canonicalize_smiles(smiles: str):
146
+ mol = Chem.MolFromSmiles(smiles)
147
+ if mol is None:
148
+ raise HTTPException(status_code=400, detail="Invalid SMILES.")
149
+ canonical = Chem.MolToSmiles(mol, isomericSmiles=True)
150
+ return mol, canonical
151
+
152
+
153
+ def make_3d_mol(smiles: str):
154
+ mol, canonical = canonicalize_smiles(smiles)
155
+ mol = Chem.AddHs(mol)
156
+
157
+ params = AllChem.ETKDGv3()
158
+ params.randomSeed = 42
159
+
160
+ status = AllChem.EmbedMolecule(mol, params)
161
+ if status != 0:
162
+ raise HTTPException(status_code=500, detail="3D embedding failed.")
163
+
164
+ try:
165
+ AllChem.UFFOptimizeMolecule(mol, maxIters=500)
166
+ except Exception:
167
+ pass
168
+
169
+ return mol, canonical
170
+
171
+
172
+ def write_sdf_for_conversion(mol, sdf_path: Path):
173
+ writer = Chem.SDWriter(str(sdf_path))
174
+ writer.write(mol)
175
+ writer.close()
176
+
177
+
178
+ def export_pdb(mol, out_path: Path):
179
+ Chem.MolToPDBFile(mol, str(out_path))
180
+
181
+
182
+ def export_pdbqt(sdf_path: Path, out_path: Path):
183
+ mk_prepare = shutil.which("mk_prepare_ligand.py") or shutil.which("mk_prepare_ligand")
184
+ if mk_prepare is None:
185
+ raise HTTPException(
186
+ status_code=500,
187
+ detail="Meeko mk_prepare_ligand.py was not found in PATH. Required for PDBQT export."
188
+ )
189
+
190
+ result = run_cmd([
191
+ mk_prepare,
192
+ "-i", str(sdf_path),
193
+ "-o", str(out_path),
194
+ ])
195
+
196
+ if result.returncode != 0 or not out_path.exists():
197
+ raise HTTPException(
198
+ status_code=500,
199
+ detail=f"PDBQT export failed: {result.stdout[:1000]}"
200
+ )
201
+
202
+
203
+ def export_mol2(sdf_path: Path, out_path: Path):
204
+ obabel = shutil.which("obabel")
205
+ if obabel is None:
206
+ raise HTTPException(
207
+ status_code=500,
208
+ detail="Open Babel obabel was not found in PATH. Required for MOL2 export."
209
+ )
210
+
211
+ result = run_cmd([
212
+ obabel,
213
+ "-isdf", str(sdf_path),
214
+ "-omol2",
215
+ "-O", str(out_path),
216
+ ])
217
+
218
+ if result.returncode != 0 or not out_path.exists():
219
+ raise HTTPException(
220
+ status_code=500,
221
+ detail=f"MOL2 export failed: {result.stdout[:1000]}"
222
+ )
223
+
224
+
225
+ def count_docked(df: pd.DataFrame) -> int:
226
+ if "docking_status" not in df.columns:
227
+ return 0
228
+ return int((df["docking_status"] == "completed").sum())
229
+
230
+ def job_status_path(job_id: str) -> Path:
231
+ return JOBS_DIR / job_id / "job_status.json"
232
+
233
+
234
+ def make_api_url(path: str) -> str:
235
+ if not path.startswith("/"):
236
+ path = "/" + path
237
+ return f"{PUBLIC_BASE_URL}{path}"
238
+
239
+
240
+ def write_job_status(job_id: str, payload: Dict[str, Any]):
241
+ job_dir = JOBS_DIR / job_id
242
+ job_dir.mkdir(parents=True, exist_ok=True)
243
+
244
+ base = {
245
+ "job_id": job_id,
246
+ "status_url": make_api_url(f"/jobs/{job_id}"),
247
+ "result_url": make_api_url(f"/jobs/{job_id}/result"),
248
+ }
249
+ base.update(payload)
250
+
251
+ path = job_status_path(job_id)
252
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
253
+
254
+ tmp_path.write_text(
255
+ json.dumps(base, indent=2, ensure_ascii=False),
256
+ encoding="utf-8"
257
+ )
258
+
259
+ tmp_path.replace(path)
260
+
261
+
262
+ def read_job_status(job_id: str) -> Dict[str, Any]:
263
+ path = job_status_path(job_id)
264
+ if not path.exists():
265
+ raise HTTPException(status_code=404, detail="Job not found.")
266
+
267
+ last_error = None
268
+
269
+ for _ in range(5):
270
+ try:
271
+ text = path.read_text(encoding="utf-8").strip()
272
+ if text:
273
+ return json.loads(text)
274
+ except json.JSONDecodeError as exc:
275
+ last_error = exc
276
+
277
+ time.sleep(0.1)
278
+
279
+ if last_error:
280
+ return {
281
+ "job_id": job_id,
282
+ "status_url": make_api_url(f"/jobs/{job_id}"),
283
+ "result_url": make_api_url(f"/jobs/{job_id}/result"),
284
+ "status": "running",
285
+ "stage": "status_update",
286
+ "message": "Job status is being updated. Retry shortly."
287
+ }
288
+
289
+ return {
290
+ "job_id": job_id,
291
+ "status_url": make_api_url(f"/jobs/{job_id}"),
292
+ "result_url": make_api_url(f"/jobs/{job_id}/result"),
293
+ "status": "running",
294
+ "stage": "status_update",
295
+ "message": "Job status is not ready yet."
296
+ }
297
+
298
+ def run_generate_job(job_id: str, req_data: Dict[str, Any]):
299
+ job_dir = JOBS_DIR / job_id
300
+
301
+ try:
302
+ req = GenerateRequest(**req_data)
303
+
304
+ write_job_status(job_id, {
305
+ "status": "running",
306
+ "stage": "sampling",
307
+ "message": "Running REINVENT molecule generation",
308
+ "request": req_data,
309
+ })
310
+
311
+ runtime_config = write_runtime_sampling_config(job_dir, req.num_molecules)
312
+
313
+ run_reinvent_sampling(runtime_config, job_dir)
314
+
315
+ write_job_status(job_id, {
316
+ "status": "running",
317
+ "stage": "enrichment",
318
+ "message": "Running RDKit descriptors and DeepPurpose predictions",
319
+ "request": req_data,
320
+ })
321
+
322
+ enriched_csv = run_enrichment(job_dir, req.return_top_k)
323
+
324
+ write_job_status(job_id, {
325
+ "status": "running",
326
+ "stage": "docking",
327
+ "message": f"Running docking mode: {req.docking_mode}",
328
+ "request": req_data,
329
+ })
330
+
331
+ final_csv = run_optional_docking(job_dir, enriched_csv, req)
332
+
333
+ df = pd.read_csv(final_csv)
334
+
335
+ if "docking_pose_file" in df.columns:
336
+ df = df.drop(columns=["docking_pose_file"])
337
+
338
+ clean_csv = job_dir / "generated_results.csv"
339
+ clean_json = job_dir / "generated_results.json"
340
+
341
+ df.to_csv(clean_csv, index=False)
342
+
343
+ results = dataframe_to_public_records(df)
344
+
345
+ result_payload = {
346
+ "job_id": job_id,
347
+ "status": "completed",
348
+ "preset": req.preset,
349
+ "docking_mode": req.docking_mode,
350
+ "summary": {
351
+ "num_requested": req.num_molecules,
352
+ "num_generated": int(len(pd.read_csv(job_dir / "generated_smiles.csv"))),
353
+ "num_valid": int((df["valid"] == True).sum()) if "valid" in df.columns else None,
354
+ "num_returned": int(len(df)),
355
+ "num_docked": count_docked(df),
356
+ },
357
+ "files": {
358
+ "csv": make_file_meta(job_id, clean_csv),
359
+ "json": make_file_meta(job_id, clean_json),
360
+ },
361
+ "results": results,
362
+ "warnings": [
363
+ "Outputs are computational predictions only."
364
+ ],
365
+ }
366
+
367
+ clean_json.write_text(
368
+ json.dumps(result_payload, indent=2, ensure_ascii=False),
369
+ encoding="utf-8"
370
+ )
371
+
372
+ write_job_status(job_id, {
373
+ "status": "completed",
374
+ "stage": "completed",
375
+ "message": "Generation job completed",
376
+ "request": req_data,
377
+ "summary": result_payload["summary"],
378
+ "files": result_payload["files"],
379
+ })
380
+
381
+ except Exception as e:
382
+ if isinstance(e, HTTPException):
383
+ detail = e.detail
384
+ else:
385
+ detail = str(e)
386
+
387
+ write_job_status(job_id, {
388
+ "status": "failed",
389
+ "stage": "failed",
390
+ "message": "Generation job failed",
391
+ "request": req_data,
392
+ "error": detail,
393
+ })
394
+
395
+ # -----------------------------
396
+ # Endpoints
397
+ # -----------------------------
398
+
399
+ @app.get("/health")
400
+ def health():
401
+ return {
402
+ "status": "ok",
403
+ "bundle_exists": BUNDLE_DIR.exists(),
404
+ "models_generator_exists": (BUNDLE_DIR / "models" / "generator" / "egfr_generator.chkpt").exists(),
405
+ "models_affinity_exists": (BUNDLE_DIR / "models" / "affinity" / "model.pt").exists(),
406
+ "docking_grid_exists": (BUNDLE_DIR / "docking" / "maps_current" / "4WKQ_receptor_v5_SBr.maps.fld").exists(),
407
+ "public_base_url": PUBLIC_BASE_URL,
408
+ "reinvent_device": REINVENT_DEVICE,
409
+ "adgpu_bin": ADGPU_BIN or None,
410
+ }
411
+
412
+
413
+ @app.post("/reinvent_predict")
414
+ async def reinvent_predict(request: Request):
415
+ if not HAS_AFFINITY:
416
+ return JSONResponse(
417
+ status_code=503,
418
+ content={"error": "Affinity prediction service not available (DeepPurpose not installed)"}
419
+ )
420
+ payload = await request.json()
421
+ smiles = payload.get("smiles", []) if isinstance(payload, dict) else []
422
+ preds = predict_smiles_list(smiles)
423
+ return {"pred_pAff_mean": preds}
424
+
425
+
426
+ @app.get("/files/jobs/{job_id}/{path:path}")
427
+ def get_job_file(job_id: str, path: str):
428
+ root = (JOBS_DIR / job_id).resolve()
429
+ target = (root / path).resolve()
430
+
431
+ if not str(target).startswith(str(root)):
432
+ raise HTTPException(status_code=400, detail="Invalid file path.")
433
+
434
+ if not target.exists() or not target.is_file():
435
+ raise HTTPException(status_code=404, detail="File not found.")
436
+
437
+ return FileResponse(
438
+ path=str(target),
439
+ filename=target.name,
440
+ media_type="application/octet-stream"
441
+ )
442
+
443
+
444
+ @app.post("/ligands/export")
445
+ def export_ligand(req: LigandExportRequest):
446
+ job_id = new_job_id("lig")
447
+ job_dir = JOBS_DIR / job_id
448
+ job_dir.mkdir(parents=True, exist_ok=True)
449
+
450
+ mol, canonical = make_3d_mol(req.smiles)
451
+
452
+ sdf_path = job_dir / "ligand_3d.sdf"
453
+ write_sdf_for_conversion(mol, sdf_path)
454
+
455
+ if req.format == "pdb":
456
+ out_path = job_dir / "ligand_3d.pdb"
457
+ export_pdb(mol, out_path)
458
+
459
+ elif req.format == "pdbqt":
460
+ out_path = job_dir / "ligand_3d.pdbqt"
461
+ export_pdbqt(sdf_path, out_path)
462
+
463
+ elif req.format == "mol2":
464
+ out_path = job_dir / "ligand_3d.mol2"
465
+ export_mol2(sdf_path, out_path)
466
+
467
+ else:
468
+ raise HTTPException(status_code=400, detail="Unsupported format.")
469
+
470
+ meta = make_file_meta(job_id, out_path)
471
+ meta["format"] = req.format
472
+
473
+ return {
474
+ "job_id": job_id,
475
+ "status": "completed",
476
+ "canonical_smiles": canonical,
477
+ "format": req.format,
478
+ "file": meta,
479
+ }
480
+
481
+
482
+ def write_runtime_sampling_config(job_dir: Path, num_molecules: int) -> Path:
483
+ """
484
+ Create a job-specific REINVENT sampling config.
485
+ Keeps bundle config untouched.
486
+ """
487
+ template_path = BUNDLE_DIR / "configs" / "sampling_model.bundle.toml"
488
+ if not template_path.exists():
489
+ raise HTTPException(status_code=500, detail=f"Sampling config not found: {template_path}")
490
+
491
+ text = template_path.read_text(encoding="utf-8")
492
+
493
+ # Force runtime device and job-specific outputs.
494
+ text = text.replace('device = "cuda:0"', f'device = "{REINVENT_DEVICE}"')
495
+ text = text.replace('json_out_config = "outputs/generate/sampling_resolved.json"', f'json_out_config = "{(job_dir / "sampling_resolved.json").as_posix()}"')
496
+ text = text.replace('model_file = "models/generator/egfr_generator.chkpt"', f'model_file = "{(BUNDLE_DIR / "models" / "generator" / "egfr_generator.chkpt").as_posix()}"')
497
+ text = text.replace('output_file = "outputs/generate/generated_smiles.csv"', f'output_file = "{(job_dir / "generated_smiles.csv").as_posix()}"')
498
+
499
+ # Replace num_smiles line safely.
500
+ lines = []
501
+ replaced = False
502
+ for line in text.splitlines():
503
+ if line.strip().startswith("num_smiles"):
504
+ lines.append(f"num_smiles = {int(num_molecules)}")
505
+ replaced = True
506
+ else:
507
+ lines.append(line)
508
+
509
+ if not replaced:
510
+ lines.append(f"num_smiles = {int(num_molecules)}")
511
+
512
+ runtime_config = job_dir / "sampling_runtime.toml"
513
+ runtime_config.write_text("\n".join(lines) + "\n", encoding="utf-8")
514
+ return runtime_config
515
+
516
+
517
+ def find_reinvent_command() -> List[str]:
518
+ """
519
+ Try common REINVENT4 entrypoints.
520
+ Docker will normally expose `reinvent`.
521
+ Local fallback may use python -m reinvent.Reinvent.
522
+ """
523
+ if shutil.which("reinvent"):
524
+ return ["reinvent"]
525
+
526
+ return [os.getenv("PYTHON", "python"), "-m", "reinvent.Reinvent"]
527
+
528
+
529
+ def run_reinvent_sampling(runtime_config: Path, job_dir: Path):
530
+ cmd = find_reinvent_command() + ["-l", str(job_dir / "reinvent_sampling.log"), str(runtime_config)]
531
+
532
+ result = run_cmd(cmd, cwd=BUNDLE_DIR, timeout=1800)
533
+
534
+ log_path = job_dir / "reinvent_command_output.log"
535
+ log_path.write_text(result.stdout or "", encoding="utf-8", errors="replace")
536
+
537
+ if result.returncode != 0:
538
+ raise HTTPException(
539
+ status_code=500,
540
+ detail={
541
+ "message": "REINVENT sampling failed.",
542
+ "command": " ".join(cmd),
543
+ "log_tail": (result.stdout or "")[-2000:],
544
+ }
545
+ )
546
+
547
+
548
+ def run_enrichment(job_dir: Path, return_top_k: int) -> Path:
549
+ input_csv = job_dir / "generated_smiles.csv"
550
+ enriched_csv = job_dir / "generated_smiles_enriched.csv"
551
+
552
+ if not input_csv.exists():
553
+ raise HTTPException(status_code=500, detail=f"Generated SMILES CSV not found: {input_csv}")
554
+
555
+ script = BUNDLE_DIR / "tools" / "enrich_generated.py"
556
+ if not script.exists():
557
+ raise HTTPException(status_code=500, detail=f"Enrichment script not found: {script}")
558
+
559
+ cmd = [
560
+ os.getenv("PYTHON", "python"),
561
+ str(script),
562
+ "--input", str(input_csv),
563
+ "--output", str(enriched_csv),
564
+ "--affinity-url", DEEPPURPOSE_URL,
565
+ "--top-k", str(return_top_k),
566
+ ]
567
+
568
+ result = run_cmd(cmd, cwd=BUNDLE_DIR, timeout=1800)
569
+
570
+ (job_dir / "enrichment.log").write_text(result.stdout or "", encoding="utf-8", errors="replace")
571
+
572
+ if result.returncode != 0:
573
+ raise HTTPException(
574
+ status_code=500,
575
+ detail={
576
+ "message": "Enrichment failed.",
577
+ "command": " ".join(cmd),
578
+ "log_tail": (result.stdout or "")[-2000:],
579
+ }
580
+ )
581
+
582
+ return enriched_csv
583
+
584
+
585
+ def run_optional_docking(job_dir: Path, enriched_csv: Path, req: GenerateRequest) -> Path:
586
+ final_csv = job_dir / "generated_results.csv"
587
+
588
+ script = BUNDLE_DIR / "tools" / "dock_enriched.py"
589
+ if not script.exists():
590
+ raise HTTPException(status_code=500, detail=f"Docking script not found: {script}")
591
+
592
+ grid_file = BUNDLE_DIR / "docking" / "maps_current" / "4WKQ_receptor_v5_SBr.maps.fld"
593
+
594
+ cmd = [
595
+ os.getenv("PYTHON", "python"),
596
+ str(script),
597
+ "--input", str(enriched_csv),
598
+ "--output", str(final_csv),
599
+ "--docking-mode", req.docking_mode,
600
+ "--dock-top-k", str(req.dock_top_k),
601
+ "--grid-file", str(grid_file),
602
+ "--work-dir", str(job_dir / "docking"),
603
+ ]
604
+
605
+ if req.docking_mode != "off":
606
+ if not ADGPU_BIN:
607
+ raise HTTPException(
608
+ status_code=500,
609
+ detail="Docking requested, but ADGPU_BIN is not set. Set AutoDock-GPU binary path in production."
610
+ )
611
+ cmd += ["--adgpu-bin", ADGPU_BIN]
612
+
613
+ result = run_cmd(cmd, cwd=BUNDLE_DIR, timeout=7200)
614
+
615
+ (job_dir / "docking.log").write_text(result.stdout or "", encoding="utf-8", errors="replace")
616
+
617
+ if result.returncode != 0:
618
+ raise HTTPException(
619
+ status_code=500,
620
+ detail={
621
+ "message": "Docking step failed.",
622
+ "command": " ".join(cmd),
623
+ "log_tail": (result.stdout or "")[-2000:],
624
+ }
625
+ )
626
+
627
+ return final_csv
628
+
629
+
630
+ @app.post("/generate", status_code=202)
631
+ def submit_generate(req: GenerateRequest, background_tasks: BackgroundTasks):
632
+ job_id = new_job_id("gen")
633
+ job_dir = JOBS_DIR / job_id
634
+ job_dir.mkdir(parents=True, exist_ok=True)
635
+
636
+ req_data = req.model_dump()
637
+
638
+ write_job_status(job_id, {
639
+ "status": "queued",
640
+ "stage": "queued",
641
+ "message": "Generation job accepted and queued",
642
+ "request": req_data,
643
+ })
644
+
645
+ background_tasks.add_task(run_generate_job, job_id, req_data)
646
+
647
+ return {
648
+ "job_id": job_id,
649
+ "status": "queued",
650
+ "message": "Generation job accepted. Poll status_url until completed.",
651
+ "status_url": make_api_url(f"/jobs/{job_id}"),
652
+ "result_url": make_api_url(f"/jobs/{job_id}/result"),
653
+ }
654
+
655
+
656
+ @app.get("/jobs/{job_id}")
657
+ def get_job_status(job_id: str):
658
+ return read_job_status(job_id)
659
+
660
+
661
+ @app.get("/jobs/{job_id}/result")
662
+ def get_job_result(job_id: str):
663
+ status = read_job_status(job_id)
664
+
665
+ if status.get("status") == "failed":
666
+ return JSONResponse(status_code=500, content=status)
667
+
668
+ if status.get("status") != "completed":
669
+ return JSONResponse(status_code=202, content=status)
670
+
671
+ result_path = JOBS_DIR / job_id / "generated_results.json"
672
+ if not result_path.exists():
673
+ raise HTTPException(status_code=404, detail="Result file not found.")
674
+
675
+ return json.loads(result_path.read_text(encoding="utf-8"))
bundle/.gitattributes ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.avro filter=lfs diff=lfs merge=lfs -text
4
+ *.bin filter=lfs diff=lfs merge=lfs -text
5
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
6
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
7
+ *.ftz filter=lfs diff=lfs merge=lfs -text
8
+ *.gz filter=lfs diff=lfs merge=lfs -text
9
+ *.h5 filter=lfs diff=lfs merge=lfs -text
10
+ *.joblib filter=lfs diff=lfs merge=lfs -text
11
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
12
+ *.lz4 filter=lfs diff=lfs merge=lfs -text
13
+ *.mds filter=lfs diff=lfs merge=lfs -text
14
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
15
+ *.model filter=lfs diff=lfs merge=lfs -text
16
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
17
+ *.npy filter=lfs diff=lfs merge=lfs -text
18
+ *.npz filter=lfs diff=lfs merge=lfs -text
19
+ *.onnx filter=lfs diff=lfs merge=lfs -text
20
+ *.ot filter=lfs diff=lfs merge=lfs -text
21
+ *.parquet filter=lfs diff=lfs merge=lfs -text
22
+ *.pb filter=lfs diff=lfs merge=lfs -text
23
+ *.pickle filter=lfs diff=lfs merge=lfs -text
24
+ *.pkl filter=lfs diff=lfs merge=lfs -text
25
+ *.pt filter=lfs diff=lfs merge=lfs -text
26
+ *.pth filter=lfs diff=lfs merge=lfs -text
27
+ *.rar filter=lfs diff=lfs merge=lfs -text
28
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
29
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
30
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
31
+ *.tar filter=lfs diff=lfs merge=lfs -text
32
+ *.tflite filter=lfs diff=lfs merge=lfs -text
33
+ *.tgz filter=lfs diff=lfs merge=lfs -text
34
+ *.wasm filter=lfs diff=lfs merge=lfs -text
35
+ *.xz filter=lfs diff=lfs merge=lfs -text
36
+ *.zip filter=lfs diff=lfs merge=lfs -text
37
+ *.zst filter=lfs diff=lfs merge=lfs -text
38
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
39
+ # Audio files - uncompressed
40
+ *.pcm filter=lfs diff=lfs merge=lfs -text
41
+ *.sam filter=lfs diff=lfs merge=lfs -text
42
+ *.raw filter=lfs diff=lfs merge=lfs -text
43
+ # Audio files - compressed
44
+ *.aac filter=lfs diff=lfs merge=lfs -text
45
+ *.flac filter=lfs diff=lfs merge=lfs -text
46
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
47
+ *.ogg filter=lfs diff=lfs merge=lfs -text
48
+ *.wav filter=lfs diff=lfs merge=lfs -text
49
+ # Image files - uncompressed
50
+ *.bmp filter=lfs diff=lfs merge=lfs -text
51
+ *.gif filter=lfs diff=lfs merge=lfs -text
52
+ *.png filter=lfs diff=lfs merge=lfs -text
53
+ *.tiff filter=lfs diff=lfs merge=lfs -text
54
+ # Image files - compressed
55
+ *.jpg filter=lfs diff=lfs merge=lfs -text
56
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
57
+ *.webp filter=lfs diff=lfs merge=lfs -text
58
+ # Video files - compressed
59
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
60
+ *.webm filter=lfs diff=lfs merge=lfs -text
61
+ models/generator/egfr_generator.chkpt filter=lfs diff=lfs merge=lfs -text
62
+ models/generator/reinvent.prior filter=lfs diff=lfs merge=lfs -text
bundle/README.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # EGFR Backend Bundle
2
+
3
+ This folder contains the packaged backend assets required by the Ailixir EGFR generation pipeline.
4
+
5
+ ## Components
6
+
7
+ - Generator checkpoint
8
+ - Sampling configuration
9
+ - DeepPurpose affinity model files
10
+ - EGFR target sequence
11
+ - Docking grid/maps
12
+ - Backend utility tools
13
+
14
+ ## Notes
15
+
16
+ This bundle is intended for backend integration and Docker-based execution.
17
+
18
+ AutoDock-GPU docking is supported by the API container when running in an NVIDIA GPU Docker environment.
bundle/configs/backend_contract.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "project": "EGFR AI backend bundle",
3
+ "version": "v1-minimal",
4
+ "modes": {
5
+ "generate": {
6
+ "type": "sampling_from_packaged_generator",
7
+ "config": "configs/sampling_model.bundle.toml",
8
+ "checkpoint": "models/generator/egfr_generator.chkpt",
9
+ "allowed_user_inputs": [
10
+ "preset",
11
+ "num_molecules",
12
+ "return_top_k"
13
+ ],
14
+ "do_not_expose": [
15
+ "raw TOML",
16
+ "checkpoint paths",
17
+ "training stages",
18
+ "RL scoring strategy",
19
+ "batch_size",
20
+ "bucket_size"
21
+ ],
22
+ "postprocess": {
23
+ "script": "tools/enrich_generated.py",
24
+ "input": "outputs/generate/generated_smiles.csv",
25
+ "output": "outputs/generate/generated_smiles_enriched.csv",
26
+ "adds_columns": [
27
+ "valid",
28
+ "canonical_smiles",
29
+ "mw",
30
+ "logp",
31
+ "tpsa",
32
+ "hbd",
33
+ "hba",
34
+ "rot_bonds",
35
+ "qed",
36
+ "sa_score",
37
+ "pred_pAff_mean"
38
+ ],
39
+ "sort_note": "Display sorting may use pred_pAff_mean desc then QED desc. This is not a final scientific ranking."
40
+ }
41
+ },
42
+ "score_smiles": {
43
+ "type": "affinity_api",
44
+ "run_command": "uvicorn services.deeppurpose.serve_affinity:app --host 127.0.0.1 --port 8001",
45
+ "health_endpoint": "GET /health",
46
+ "predict_endpoint": "POST /reinvent_predict",
47
+ "request_example": {
48
+ "smiles": [
49
+ "CCO",
50
+ "c1ccccc1"
51
+ ]
52
+ },
53
+ "response_key": "pred_pAff_mean"
54
+ },
55
+ "docking": {
56
+ "enabled_by_default": false,
57
+ "allowed_modes": [
58
+ "off",
59
+ "top_k",
60
+ "all"
61
+ ],
62
+ "recommended_mode": "top_k",
63
+ "top_k_default": 10,
64
+ "maps_dir": "docking/maps_current",
65
+ "grid_file": "docking/maps_current/4WKQ_receptor_v5_SBr.maps.fld",
66
+ "binary_env_var": "ADGPU_BIN",
67
+ "result_columns": [
68
+ "docking_score",
69
+ "docking_status",
70
+ "docking_pose_file"
71
+ ],
72
+ "when_disabled": {
73
+ "docking_score": null,
74
+ "docking_status": "not_run",
75
+ "docking_pose_file": null
76
+ },
77
+ "note": "Docking is optional and backend-controlled. Use top_k for demos because docking is slower than descriptor and affinity scoring."
78
+ }
79
+ }
80
+ }
bundle/configs/sampling_model.bundle.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal deployment sampling config
2
+ # Samples molecules from the packaged EGFR generator checkpoint.
3
+ # Does not expose the original training/RL strategy.
4
+
5
+ run_type = "sampling"
6
+ device = "cuda:0"
7
+
8
+ json_out_config = "outputs/generate/sampling_resolved.json"
9
+
10
+ [parameters]
11
+ model_file = "models/generator/egfr_generator.chkpt"
12
+ output_file = "outputs/generate/generated_smiles.csv"
13
+
14
+ num_smiles = 100
15
+ unique_molecules = true
16
+ randomize_smiles = true
bundle/docking/maps_current/4WKQ_clean.pdb ADDED
The diff for this file is too large to render. See raw diff
 
bundle/docking/maps_current/4WKQ_raw.pdb ADDED
The diff for this file is too large to render. See raw diff
 
bundle/docking/maps_current/4WKQ_receptor.A.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:54b59003346b5d5df1c52329a01a0792259cf10cddaaf3cbf45aeda792470e4a
3
+ size 2049653
bundle/docking/maps_current/4WKQ_receptor.Br.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2a39ee75ecf7590c47b095ae19325c5ddf83b1a5843049ae8b2f19904986821d
3
+ size 2069949
bundle/docking/maps_current/4WKQ_receptor.C.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:78b2d1b94f8b266ba805d37376b010bad13411c43900d5d5d71899f0ac8f5fd8
3
+ size 2052665
bundle/docking/maps_current/4WKQ_receptor.Cl.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:59adaf85853d0ea362160d08d5af5516fbfaf5d25743884f0c2a8dcbecb14f38
3
+ size 2082114
bundle/docking/maps_current/4WKQ_receptor.F.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b5ea3924fbd0f7b8acd4b7f44d90f4e6474fead2ce0e9ab79d07de50b628a48c
3
+ size 1954132
bundle/docking/maps_current/4WKQ_receptor.HD.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c9c9269b5133820343a14551159aa1f6eefea6308cf1b47245a57fec326b7f6f
3
+ size 1820642
bundle/docking/maps_current/4WKQ_receptor.N.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:06b3ba3e6dc84c6d2b5f91d2026c18626cb80861fb8d7ea63e9d0c21fef6863e
3
+ size 2012924
bundle/docking/maps_current/4WKQ_receptor.NA.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f55ea020098731262b5965462d77bb5d678411d5dcf22dd1c5bde218331fd50
3
+ size 2014019
bundle/docking/maps_current/4WKQ_receptor.OA.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:69594facd6e979219f24134b0dede413b257b2b4043c746e8349f092a9800a40
3
+ size 2000447
bundle/docking/maps_current/4WKQ_receptor.S.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1ce99441b16d1c0b3cd87ecb4488e7fbff4bb55a830e0e904bf34747e90c167
3
+ size 2065476
bundle/docking/maps_current/4WKQ_receptor.SA.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f10253e8b726f975329cc64e16308ddf1a24cd48f6a68f02d60725a484a7e39c
3
+ size 2115792
bundle/docking/maps_current/4WKQ_receptor.d.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:880b0e603daa5b8bd200c44753c43d9ce1209c98a68fa1954d23e78d5715a1d1
3
+ size 1599272
bundle/docking/maps_current/4WKQ_receptor.e.map ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:54a19cf5caa31d2be5fa3936c7f1bb205360022447094637ac46bea02b05191a
3
+ size 1839643
bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.fld ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AVS field file
2
+ #
3
+ # AutoDock Atomic Affinity and Electrostatic Grids
4
+ #
5
+ # Created by autogrid4.
6
+ #
7
+ #SPACING 0.375
8
+ #NELEMENTS 64 64 64
9
+ #CENTER 7.375 194.927 16.540
10
+ #MACROMOLECULE receptorH_rigid.pdbqt
11
+ #GRID_PARAMETER_FILE receptorH.gpf
12
+ #
13
+ ndim=3 # number of dimensions in the field
14
+ dim1=65 # number of x-elements
15
+ dim2=65 # number of y-elements
16
+ dim3=65 # number of z-elements
17
+ nspace=3 # number of physical coordinates per point
18
+ veclen=13 # number of affinity values at each point
19
+ data=float # data type (byte, integer, float, double)
20
+ field=uniform # field type (uniform, rectilinear, irregular)
21
+ coord 1 file=4WKQ_receptor_v5_SBr.maps.xyz filetype=ascii offset=0
22
+ coord 2 file=4WKQ_receptor_v5_SBr.maps.xyz filetype=ascii offset=2
23
+ coord 3 file=4WKQ_receptor_v5_SBr.maps.xyz filetype=ascii offset=4
24
+ label=A-affinity # component label for variable 1
25
+ label=C-affinity # component label for variable 2
26
+ label=F-affinity # component label for variable 3
27
+ label=N-affinity # component label for variable 4
28
+ label=NA-affinity # component label for variable 5
29
+ label=Cl-affinity # component label for variable 6
30
+ label=OA-affinity # component label for variable 7
31
+ label=HD-affinity # component label for variable 8
32
+ label=S-affinity # component label for variable 9
33
+ label=SA-affinity # component label for variable 10
34
+ label=Br-affinity # component label for variable 11
35
+ label=Electrostatics # component label for variable 11
36
+ label=Desolvation # component label for variable 12
37
+ #
38
+ # location of affinity grid files and how to read them
39
+ #
40
+ variable 1 file=4WKQ_receptor.A.map filetype=ascii skip=6
41
+ variable 2 file=4WKQ_receptor.C.map filetype=ascii skip=6
42
+ variable 3 file=4WKQ_receptor.F.map filetype=ascii skip=6
43
+ variable 4 file=4WKQ_receptor.N.map filetype=ascii skip=6
44
+ variable 5 file=4WKQ_receptor.NA.map filetype=ascii skip=6
45
+ variable 6 file=4WKQ_receptor.Cl.map filetype=ascii skip=6
46
+ variable 7 file=4WKQ_receptor.OA.map filetype=ascii skip=6
47
+ variable 8 file=4WKQ_receptor.HD.map filetype=ascii skip=6
48
+ variable 9 file=4WKQ_receptor.S.map filetype=ascii skip=6
49
+ variable 10 file=4WKQ_receptor.Br.map filetype=ascii skip=6
50
+ variable 11 file=4WKQ_receptor.SA.map filetype=ascii skip=6
51
+ variable 12 file=4WKQ_receptor.e.map filetype=ascii skip=6
52
+ variable 13 file=4WKQ_receptor.d.map filetype=ascii skip=6
bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.xyz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ -4.625 19.375
2
+ 182.927 206.927
3
+ 4.540 28.540
bundle/docking/maps_current/autogrid_v9.log ADDED
The diff for this file is too large to render. See raw diff
 
bundle/docking/maps_current/ligand.sdf ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ RDKit 3D
3
+
4
+ 103108 0 0 0 0 0 0 0 0999 V2000
5
+ 14.9159 0.7454 -1.8018 C 0 0 0 0 0 0 0 0 0 0 0 0
6
+ 13.5628 0.2305 -1.3211 C 0 0 0 0 0 0 0 0 0 0 0 0
7
+ 13.2828 0.6808 0.0443 N 0 0 0 0 0 0 0 0 0 0 0 0
8
+ 12.0685 0.3583 0.7486 C 0 0 0 0 0 0 0 0 0 0 0 0
9
+ 11.8744 0.8586 2.0450 C 0 0 0 0 0 0 0 0 0 0 0 0
10
+ 10.7055 0.5671 2.7510 C 0 0 0 0 0 0 0 0 0 0 0 0
11
+ 9.7139 -0.2293 2.1744 C 0 0 0 0 0 0 0 0 0 0 0 0
12
+ 8.5844 -0.4901 2.8801 F 0 0 0 0 0 0 0 0 0 0 0 0
13
+ 9.8899 -0.7448 0.8778 C 0 0 0 0 0 0 0 0 0 0 0 0
14
+ 8.8269 -1.5940 0.2244 C 0 0 0 0 0 0 0 0 0 0 0 0
15
+ 8.2308 -0.8951 -0.9367 N 0 0 0 0 0 0 0 0 0 0 0 0
16
+ 7.5068 -1.8162 -1.8550 C 0 0 0 0 0 0 0 0 0 0 0 0
17
+ 5.9734 -1.6512 -1.8400 C 0 0 0 0 0 0 0 0 0 0 0 0
18
+ 5.4710 -1.3601 -0.4180 C 0 0 0 0 0 0 0 0 0 0 0 0
19
+ 3.9299 -1.3365 -0.3465 C 0 0 0 0 0 0 0 0 0 0 0 0
20
+ 3.3592 -0.3912 -1.3213 N 0 0 0 0 0 0 0 0 0 0 0 0
21
+ 1.9654 -0.7133 -1.6653 C 0 0 0 0 0 0 0 0 0 0 0 0
22
+ 0.9998 -0.4757 -0.4934 C 0 0 0 0 0 0 0 0 0 0 0 0
23
+ -0.3856 -0.6700 -0.9425 N 0 0 0 0 0 0 0 0 0 0 0 0
24
+ -1.3427 -0.3567 0.1329 C 0 0 0 0 0 0 0 0 0 0 0 0
25
+ -1.4734 1.1548 0.3590 C 0 0 0 0 0 0 0 0 0 0 0 0
26
+ -2.9341 1.3957 0.7147 C 0 0 0 0 0 0 0 0 0 0 0 0
27
+ -3.6481 0.2147 0.2574 N 0 0 0 0 0 0 0 0 0 0 0 0
28
+ -5.0813 0.0701 0.2617 C 0 0 0 0 0 0 0 0 0 0 0 0
29
+ -5.9154 1.0564 0.8098 C 0 0 0 0 0 0 0 0 0 0 0 0
30
+ -7.3025 0.8593 0.7886 C 0 0 0 0 0 0 0 0 0 0 0 0
31
+ -8.1997 1.8486 1.3268 N 0 0 0 0 0 0 0 0 0 0 0 0
32
+ -7.9960 2.3693 2.6856 C 0 0 0 0 0 0 0 0 0 0 0 0
33
+ -8.5366 3.8019 2.7919 C 0 0 0 0 0 0 0 0 0 0 0 0
34
+ -9.9217 3.8869 2.3059 N 0 0 0 0 0 0 0 0 0 0 0 0
35
+ -9.9981 3.4632 0.8964 C 0 0 0 0 0 0 0 0 0 0 0 0
36
+ -11.4008 3.7222 0.3029 C 0 0 0 0 0 0 0 0 0 0 0 0
37
+ -12.5458 2.9790 0.9967 C 0 0 0 0 0 0 0 0 0 0 0 0
38
+ -9.5337 2.0031 0.7368 C 0 0 0 0 0 0 0 0 0 0 0 0
39
+ -7.8044 -0.2700 0.2210 N 0 0 0 0 0 0 0 0 0 0 0 0
40
+ -6.9908 -1.2213 -0.3080 C 0 0 0 0 0 0 0 0 0 0 0 0
41
+ -7.5507 -2.4045 -0.8952 N 0 0 0 0 0 0 0 0 0 0 0 0
42
+ -6.7501 -3.4561 -1.4955 C 0 0 0 0 0 0 0 0 0 0 0 0
43
+ -7.7480 -4.4199 -2.1245 C 0 0 0 0 0 0 0 0 0 0 0 0
44
+ -9.1043 -4.0854 -1.5056 C 0 0 0 0 0 0 0 0 0 0 0 0
45
+ -8.9763 -2.6722 -0.9520 C 0 0 0 0 0 0 0 0 0 0 0 0
46
+ -5.6436 -1.0439 -0.2798 N 0 0 0 0 0 0 0 0 0 0 0 0
47
+ -2.7549 -0.8224 -0.2303 C 0 0 0 0 0 0 0 0 0 0 0 0
48
+ 6.1124 -0.0702 0.1478 C 0 0 0 0 0 0 0 0 0 0 0 0
49
+ 7.4361 0.2941 -0.5539 C 0 0 0 0 0 0 0 0 0 0 0 0
50
+ 11.0666 -0.4459 0.1707 C 0 0 0 0 0 0 0 0 0 0 0 0
51
+ 14.9260 1.8562 -1.7958 H 0 0 0 0 0 0 0 0 0 0 0 0
52
+ 15.7254 0.3669 -1.1420 H 0 0 0 0 0 0 0 0 0 0 0 0
53
+ 15.1037 0.3914 -2.8373 H 0 0 0 0 0 0 0 0 0 0 0 0
54
+ 12.7725 0.6077 -2.0055 H 0 0 0 0 0 0 0 0 0 0 0 0
55
+ 13.5712 -0.8804 -1.3511 H 0 0 0 0 0 0 0 0 0 0 0 0
56
+ 13.9929 1.2726 0.5345 H 0 0 0 0 0 0 0 0 0 0 0 0
57
+ 12.6304 1.4791 2.5097 H 0 0 0 0 0 0 0 0 0 0 0 0
58
+ 10.5677 0.9629 3.7491 H 0 0 0 0 0 0 0 0 0 0 0 0
59
+ 8.0450 -1.9017 0.9497 H 0 0 0 0 0 0 0 0 0 0 0 0
60
+ 9.3298 -2.5288 -0.1089 H 0 0 0 0 0 0 0 0 0 0 0 0
61
+ 7.8669 -1.6304 -2.8891 H 0 0 0 0 0 0 0 0 0 0 0 0
62
+ 7.7422 -2.8821 -1.6378 H 0 0 0 0 0 0 0 0 0 0 0 0
63
+ 5.6891 -0.8178 -2.5198 H 0 0 0 0 0 0 0 0 0 0 0 0
64
+ 5.5018 -2.5799 -2.2298 H 0 0 0 0 0 0 0 0 0 0 0 0
65
+ 5.7995 -2.2085 0.2238 H 0 0 0 0 0 0 0 0 0 0 0 0
66
+ 3.6142 -1.0626 0.6851 H 0 0 0 0 0 0 0 0 0 0 0 0
67
+ 3.5669 -2.3705 -0.5447 H 0 0 0 0 0 0 0 0 0 0 0 0
68
+ 3.3926 0.5782 -0.9291 H 0 0 0 0 0 0 0 0 0 0 0 0
69
+ 1.8881 -1.7648 -2.0201 H 0 0 0 0 0 0 0 0 0 0 0 0
70
+ 1.6591 -0.0640 -2.5140 H 0 0 0 0 0 0 0 0 0 0 0 0
71
+ 1.2321 -1.1828 0.3338 H 0 0 0 0 0 0 0 0 0 0 0 0
72
+ 1.1398 0.5639 -0.1255 H 0 0 0 0 0 0 0 0 0 0 0 0
73
+ -0.4920 -1.6854 -1.1794 H 0 0 0 0 0 0 0 0 0 0 0 0
74
+ -1.0441 -0.8489 1.0884 H 0 0 0 0 0 0 0 0 0 0 0 0
75
+ -1.2285 1.7061 -0.5763 H 0 0 0 0 0 0 0 0 0 0 0 0
76
+ -0.8011 1.5012 1.1738 H 0 0 0 0 0 0 0 0 0 0 0 0
77
+ -3.3115 2.3047 0.1977 H 0 0 0 0 0 0 0 0 0 0 0 0
78
+ -3.0549 1.5023 1.8146 H 0 0 0 0 0 0 0 0 0 0 0 0
79
+ -5.5015 1.9648 1.2243 H 0 0 0 0 0 0 0 0 0 0 0 0
80
+ -6.9223 2.3719 2.9653 H 0 0 0 0 0 0 0 0 0 0 0 0
81
+ -8.5321 1.7164 3.4082 H 0 0 0 0 0 0 0 0 0 0 0 0
82
+ -7.8874 4.4932 2.2104 H 0 0 0 0 0 0 0 0 0 0 0 0
83
+ -8.5009 4.1277 3.8539 H 0 0 0 0 0 0 0 0 0 0 0 0
84
+ -10.5062 3.2500 2.8966 H 0 0 0 0 0 0 0 0 0 0 0 0
85
+ -9.3002 4.0960 0.3005 H 0 0 0 0 0 0 0 0 0 0 0 0
86
+ -11.6112 4.8127 0.3544 H 0 0 0 0 0 0 0 0 0 0 0 0
87
+ -11.3963 3.4405 -0.7727 H 0 0 0 0 0 0 0 0 0 0 0 0
88
+ -12.4097 1.8807 0.9312 H 0 0 0 0 0 0 0 0 0 0 0 0
89
+ -13.5009 3.2320 0.4901 H 0 0 0 0 0 0 0 0 0 0 0 0
90
+ -12.6355 3.2857 2.0588 H 0 0 0 0 0 0 0 0 0 0 0 0
91
+ -10.2297 1.3063 1.2488 H 0 0 0 0 0 0 0 0 0 0 0 0
92
+ -9.5089 1.7490 -0.3462 H 0 0 0 0 0 0 0 0 0 0 0 0
93
+ -6.0724 -3.0348 -2.2697 H 0 0 0 0 0 0 0 0 0 0 0 0
94
+ -6.1548 -3.9670 -0.7078 H 0 0 0 0 0 0 0 0 0 0 0 0
95
+ -7.4656 -5.4776 -1.9310 H 0 0 0 0 0 0 0 0 0 0 0 0
96
+ -7.7900 -4.2490 -3.2228 H 0 0 0 0 0 0 0 0 0 0 0 0
97
+ -9.9194 -4.1525 -2.2585 H 0 0 0 0 0 0 0 0 0 0 0 0
98
+ -9.3165 -4.7935 -0.6745 H 0 0 0 0 0 0 0 0 0 0 0 0
99
+ -9.4662 -1.9399 -1.6299 H 0 0 0 0 0 0 0 0 0 0 0 0
100
+ -9.4247 -2.6080 0.0633 H 0 0 0 0 0 0 0 0 0 0 0 0
101
+ -2.9803 -1.7931 0.2631 H 0 0 0 0 0 0 0 0 0 0 0 0
102
+ -2.8810 -0.9236 -1.3309 H 0 0 0 0 0 0 0 0 0 0 0 0
103
+ 5.4300 0.7995 0.0518 H 0 0 0 0 0 0 0 0 0 0 0 0
104
+ 6.2968 -0.2059 1.2356 H 0 0 0 0 0 0 0 0 0 0 0 0
105
+ 8.0273 0.9758 0.0939 H 0 0 0 0 0 0 0 0 0 0 0 0
106
+ 7.2083 0.8779 -1.4730 H 0 0 0 0 0 0 0 0 0 0 0 0
107
+ 11.1885 -0.8433 -0.8282 H 0 0 0 0 0 0 0 0 0 0 0 0
108
+ 1 2 1 0
109
+ 2 3 1 0
110
+ 3 4 1 0
111
+ 4 5 2 0
112
+ 5 6 1 0
113
+ 6 7 2 0
114
+ 7 8 1 0
115
+ 7 9 1 0
116
+ 9 10 1 0
117
+ 10 11 1 0
118
+ 11 12 1 0
119
+ 12 13 1 0
120
+ 13 14 1 0
121
+ 14 15 1 0
122
+ 15 16 1 0
123
+ 16 17 1 0
124
+ 17 18 1 0
125
+ 18 19 1 0
126
+ 19 20 1 0
127
+ 20 21 1 0
128
+ 21 22 1 0
129
+ 22 23 1 0
130
+ 23 24 1 0
131
+ 24 25 2 0
132
+ 25 26 1 0
133
+ 26 27 1 0
134
+ 27 28 1 0
135
+ 28 29 1 0
136
+ 29 30 1 0
137
+ 30 31 1 0
138
+ 31 32 1 0
139
+ 32 33 1 0
140
+ 31 34 1 0
141
+ 26 35 2 0
142
+ 35 36 1 0
143
+ 36 37 1 0
144
+ 37 38 1 0
145
+ 38 39 1 0
146
+ 39 40 1 0
147
+ 40 41 1 0
148
+ 36 42 2 0
149
+ 23 43 1 0
150
+ 14 44 1 0
151
+ 44 45 1 0
152
+ 9 46 2 0
153
+ 46 4 1 0
154
+ 45 11 1 0
155
+ 43 20 1 0
156
+ 42 24 1 0
157
+ 34 27 1 0
158
+ 41 37 1 0
159
+ 1 47 1 0
160
+ 1 48 1 0
161
+ 1 49 1 0
162
+ 2 50 1 0
163
+ 2 51 1 0
164
+ 3 52 1 0
165
+ 5 53 1 0
166
+ 6 54 1 0
167
+ 10 55 1 0
168
+ 10 56 1 0
169
+ 12 57 1 0
170
+ 12 58 1 0
171
+ 13 59 1 0
172
+ 13 60 1 0
173
+ 14 61 1 0
174
+ 15 62 1 0
175
+ 15 63 1 0
176
+ 16 64 1 0
177
+ 17 65 1 0
178
+ 17 66 1 0
179
+ 18 67 1 0
180
+ 18 68 1 0
181
+ 19 69 1 0
182
+ 20 70 1 0
183
+ 21 71 1 0
184
+ 21 72 1 0
185
+ 22 73 1 0
186
+ 22 74 1 0
187
+ 25 75 1 0
188
+ 28 76 1 0
189
+ 28 77 1 0
190
+ 29 78 1 0
191
+ 29 79 1 0
192
+ 30 80 1 0
193
+ 31 81 1 0
194
+ 32 82 1 0
195
+ 32 83 1 0
196
+ 33 84 1 0
197
+ 33 85 1 0
198
+ 33 86 1 0
199
+ 34 87 1 0
200
+ 34 88 1 0
201
+ 38 89 1 0
202
+ 38 90 1 0
203
+ 39 91 1 0
204
+ 39 92 1 0
205
+ 40 93 1 0
206
+ 40 94 1 0
207
+ 41 95 1 0
208
+ 41 96 1 0
209
+ 43 97 1 0
210
+ 43 98 1 0
211
+ 44 99 1 0
212
+ 44100 1 0
213
+ 45101 1 0
214
+ 45102 1 0
215
+ 46103 1 0
216
+ M END
217
+ $$$$
bundle/docking/maps_current/receptorH.gpf ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ npts 64 64 64 # num.grid points in xyz
2
+ gridfld 4WKQ_receptor_v5_SBr.maps.fld # grid_data_file
3
+ spacing 0.375 # spacing(A)
4
+ receptor_types A C NA OA N SA HD # receptor atom types
5
+ ligand_types A C F N NA Cl OA HD S SA Br # ligand atom types
6
+ receptor receptorH_rigid.pdbqt # macromolecule
7
+ gridcenter 7.375 194.927 16.540 # xyz-coordinates or auto
8
+ smooth 0.5 # store minimum energy w/in rad(A)
9
+ map 4WKQ_receptor.A.map # atom-specific affinity map
10
+ map 4WKQ_receptor.C.map # atom-specific affinity map
11
+ map 4WKQ_receptor.F.map # atom-specific affinity map
12
+ map 4WKQ_receptor.N.map # atom-specific affinity map
13
+ map 4WKQ_receptor.NA.map # atom-specific affinity map
14
+ map 4WKQ_receptor.Cl.map # atom-specific affinity map
15
+ map 4WKQ_receptor.OA.map # atom-specific affinity map
16
+ map 4WKQ_receptor.HD.map # atom-specific affinity map
17
+ map 4WKQ_receptor.S.map # atom-specific affinity map
18
+ map 4WKQ_receptor.Br.map # atom-specific affinity map
19
+ map 4WKQ_receptor.SA.map # atom-specific affinity map
20
+ elecmap 4WKQ_receptor.e.map # electrostatic potential map
21
+ dsolvmap 4WKQ_receptor.d.map # desolvation potential map
22
+ dielectric -0.1465 # <0, AD4 distance-dep.diel;>0, constant
bundle/docking/maps_current/receptorH_flex.pdbqt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BEGIN_RES CYS A 797
2
+ REMARK INDEX MAP 3 1 15 2 18 3 19 4
3
+ ROOT
4
+ ATOM 1 CA CYS A 797 5.927 197.219 16.594 1.00 0.00 0.181 C
5
+ ENDROOT
6
+ BRANCH 1 2
7
+ ATOM 2 CB CYS A 797 6.611 196.143 17.446 1.00 0.00 0.101 C
8
+ BRANCH 2 3
9
+ ATOM 3 SG CYS A 797 7.375 194.927 16.540 1.00 0.00 -0.177 SA
10
+ ATOM 4 HG CYS A 797 7.947 194.016 17.389 1.00 0.00 0.102 HD
11
+ ENDBRANCH 2 3
12
+ ENDBRANCH 1 2
13
+ END_RES CYS A 797
bundle/docking/maps_current/receptorH_rigid.pdbqt ADDED
The diff for this file is too large to render. See raw diff
 
bundle/docs/API_RESPONSE_SPEC.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # API Response Specification — EGFR AI Backend Bundle
2
+
3
+ This document describes the expected response shape for the backend/frontend integration.
4
+
5
+ ## 1. Generate endpoint
6
+
7
+ ### Request
8
+
9
+ ```json
10
+ {
11
+ "preset": "egfr_generator",
12
+ "num_molecules": 100,
13
+ "return_top_k": 20,
14
+ "docking_mode": "off"
15
+ }
16
+
17
+ Allowed docking modes
18
+ off
19
+ top_k
20
+ all
21
+
22
+ Meaning:
23
+
24
+ off: no docking is run. Docking columns are returned as empty/null.
25
+ top_k: docking is run only for the top-k generated molecules after property + affinity scoring.
26
+ all: docking is run for all returned molecules. This is slower.
27
+ ##
28
+
29
+ 2. Response
30
+ **Review ai_artifacts_release\examples\generate_response.example.json
31
+ {
32
+ "job_id": "gen_001",
33
+ "preset": "egfr_generator",
34
+ "status": "completed",
35
+ "docking_mode": "off",
36
+ "summary": {
37
+ "num_generated": 100,
38
+ "num_valid": 100,
39
+ "num_returned": 20
40
+ },
41
+ "columns": [
42
+ "SMILES",
43
+ "SMILES_state",
44
+ "NLL",
45
+ "valid",
46
+ "canonical_smiles",
47
+ "mw",
48
+ "logp",
49
+ "tpsa",
50
+ "hbd",
51
+ "hba",
52
+ "rot_bonds",
53
+ "qed",
54
+ "sa_score",
55
+ "pred_pAff_mean",
56
+ "docking_score",
57
+ "docking_status",
58
+ "docking_pose_file"
59
+ ],
60
+ "results": [
61
+ {
62
+ "SMILES": "CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
63
+ "SMILES_state": 1,
64
+ "NLL": 5.71,
65
+ "valid": true,
66
+ "canonical_smiles": "CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
67
+ "mw": 358.405,
68
+ "logp": 2.788,
69
+ "tpsa": 92.69,
70
+ "hbd": 2,
71
+ "hba": 6,
72
+ "rot_bonds": 5,
73
+ "qed": 0.5698,
74
+ "sa_score": 2.5204,
75
+ "pred_pAff_mean": 11.0104,
76
+ "docking_score": null,
77
+ "docking_status": "not_run",
78
+ "docking_pose_file": null
79
+ }
80
+ ],
81
+ "warnings": [
82
+ "Outputs are computational predictions only.",
83
+ "Docking was not run for this job." ## depend on the user's preference
84
+ ]
85
+ }
86
+
87
+
88
+
89
+ 2. Score SMILES endpoint
90
+ Request
91
+ {
92
+ "smiles": [
93
+ "CCO",
94
+ "c1ccccc1"
95
+ ],
96
+ "docking_mode": "off"
97
+ }
98
+
99
+
100
+
101
+ 4. Frontend/filter recommendations
102
+
103
+ Recommended filters:
104
+
105
+ valid == true
106
+ mw between 250 and 600
107
+ logp between 0 and 6
108
+ tpsa between 40 and 140
109
+ qed >= 0.3
110
+ pred_pAff_mean available
111
+ docking_status in ["not_run", "completed"]
bundle/examples/generate_response.example.json ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "job_id": "example_generation_job",
3
+ "preset": "egfr_generator",
4
+ "status": "completed",
5
+ "docking_mode": "top_k",
6
+ "summary": {
7
+ "num_generated": 100,
8
+ "num_valid": 5,
9
+ "num_returned": 5
10
+ },
11
+ "columns": [
12
+ "SMILES",
13
+ "SMILES_state",
14
+ "NLL",
15
+ "valid",
16
+ "canonical_smiles",
17
+ "mw",
18
+ "logp",
19
+ "tpsa",
20
+ "hbd",
21
+ "hba",
22
+ "rot_bonds",
23
+ "qed",
24
+ "sa_score",
25
+ "pred_pAff_mean",
26
+ "docking_score",
27
+ "docking_status",
28
+ "docking_pose_file"
29
+ ],
30
+ "results": [
31
+ {
32
+ "SMILES": "CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
33
+ "SMILES_state": 1,
34
+ "NLL": 5.71,
35
+ "valid": true,
36
+ "canonical_smiles": "CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
37
+ "mw": 358.4050000000001,
38
+ "logp": 2.7880000000000003,
39
+ "tpsa": 92.69,
40
+ "hbd": 2,
41
+ "hba": 6,
42
+ "rot_bonds": 5,
43
+ "qed": 0.5698238625255986,
44
+ "sa_score": 2.52037562103958,
45
+ "pred_pAff_mean": 11.01038932800293,
46
+ "docking_score": -8.86,
47
+ "docking_status": "completed",
48
+ "docking_pose_file": "outputs/docking/ligand_0001/ligand_0001.xml"
49
+ },
50
+ {
51
+ "SMILES": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5cncnc5c4)c3c2)C1",
52
+ "SMILES_state": 1,
53
+ "NLL": 6.86,
54
+ "valid": true,
55
+ "canonical_smiles": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5cncnc5c4)c3c2)C1",
56
+ "mw": 413.4850000000002,
57
+ "logp": 2.7735000000000003,
58
+ "tpsa": 87.14,
59
+ "hbd": 1,
60
+ "hba": 7,
61
+ "rot_bonds": 5,
62
+ "qed": 0.5382597382382679,
63
+ "sa_score": 2.7266538303986785,
64
+ "pred_pAff_mean": 10.975865364074709,
65
+ "docking_score": -8.93,
66
+ "docking_status": "completed",
67
+ "docking_pose_file": "outputs/docking/ligand_0002/ligand_0002.xml"
68
+ },
69
+ {
70
+ "SMILES": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5c(Nc6ccc7cncnc7c6)ncnc5c4)c3c2)CC1",
71
+ "SMILES_state": 1,
72
+ "NLL": 12.81,
73
+ "valid": true,
74
+ "canonical_smiles": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5c(Nc6ccc7cncnc7c6)ncnc5c4)c3c2)CC1",
75
+ "mw": 570.6610000000001,
76
+ "logp": 4.502800000000002,
77
+ "tpsa": 124.95,
78
+ "hbd": 2,
79
+ "hba": 10,
80
+ "rot_bonds": 7,
81
+ "qed": 0.2833630346138082,
82
+ "sa_score": 2.9094321238244607,
83
+ "pred_pAff_mean": 10.92349624633789,
84
+ "docking_score": -11.13,
85
+ "docking_status": "completed",
86
+ "docking_pose_file": "outputs/docking/ligand_0003/ligand_0003.xml"
87
+ },
88
+ {
89
+ "SMILES": "CN1CCCN(CCC(=O)Nc2ccc3nncc(-c4ccc5cncnc5c4)c3c2)C1",
90
+ "SMILES_state": 1,
91
+ "NLL": 5.03,
92
+ "valid": true,
93
+ "canonical_smiles": "CN1CCCN(CCC(=O)Nc2ccc3nncc(-c4ccc5cncnc5c4)c3c2)C1",
94
+ "mw": 427.5120000000002,
95
+ "logp": 3.1636000000000006,
96
+ "tpsa": 87.14,
97
+ "hbd": 1,
98
+ "hba": 7,
99
+ "rot_bonds": 5,
100
+ "qed": 0.5234216428527144,
101
+ "sa_score": 2.7645745083533857,
102
+ "pred_pAff_mean": 10.726749420166016,
103
+ "docking_score": null,
104
+ "docking_status": "not_run",
105
+ "docking_pose_file": null
106
+ },
107
+ {
108
+ "SMILES": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5ncncc5c4)c3c2)CC1",
109
+ "SMILES_state": 1,
110
+ "NLL": 6.0,
111
+ "valid": true,
112
+ "canonical_smiles": "CN1CCN(CCC(=O)Nc2ccc3nncc(-c4ccc5ncncc5c4)c3c2)CC1",
113
+ "mw": 427.5120000000002,
114
+ "logp": 2.8160000000000007,
115
+ "tpsa": 87.14,
116
+ "hbd": 1,
117
+ "hba": 7,
118
+ "rot_bonds": 5,
119
+ "qed": 0.5240018720180243,
120
+ "sa_score": 2.5455327734925,
121
+ "pred_pAff_mean": 10.25698947906494,
122
+ "docking_score": null,
123
+ "docking_status": "not_run",
124
+ "docking_pose_file": null
125
+ }
126
+ ],
127
+ "warnings": [
128
+ "Outputs are computational predictions only.",
129
+ "Docking was run only for a top-k subset."
130
+ ]
131
+ }
bundle/models/affinity/config.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d12e50c5554fe2c7d13e5db818e78d61ff790b84b0435706e94f52bcb94d5498
3
+ size 513
bundle/models/affinity/model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:38fd867f9e4d7c352c0f7b60866ecb8e6f1d98c235490498a5ccd6871d23aafd
3
+ size 45788831
bundle/models/affinity/target_sequence.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ MRPSGTAGAALLALLAALCPASRALEEKKVCQGTSNKLTQLGTFEDHFLSLQRMFNNCEVVLGNLEITYVQRNYDLSFLKTIQEVAGYVLIALNTVERIPLENLQIIRGNMYYENSYALAVLSNYDANKTGLKELPMRNLQEILHGAVRFSNNPALCNVESIQWRDIVSSDFLSNMSMDFQNHLGSCQKCDPSCPNGSCWGAGEENCQKLTKIICAQQCSGRCRGKSPSDCCHNQCAAGCTGPRESDCLVCRKFRDEATCKDTCPPLMLYNPTTYQMDVNPEGKYSFGATCVKKCPRNYVVTDHGSCVRACGADSYEMEEDGVRKCKKCEGPCRKVCNGIGIGEFKDSLSINATNIKHFKNCTSISGDLHILPVAFRGDSFTHTPPLDPQELDILKTVKEITGFLLIQAWPENRTDLHAFENLEIIRGRTKQHGQFSLAVVSLNITSLGLRSLKEISDGDVIISGNKNLCYANTINWKKLFGTSGQKTKIISNRGENSCKATGQVCHALCSPEGCWGPEPRDCVSCRNVSRGRECVDKCNLLEGEPREFVENSECIQCHPECLPQAMNITCTGRGPDNCIQCAHYIDGPHCVKTCPAGVMGENNTLVWKYADAGHVCHLCHPNCTYGCTGPGLEGCPTNGPKIPSIATGMVGALLLLLVVALGIGLFMRRRHIVRKRTLRRLLQERELVEPLTPSGEAPNQALLRILKETEFKKIKVLGSGAFGTVYKGLWIPEGEKVKIPVAIKELREATSPKANKEILDEAYVMASVDNPHVCRLLGICLTSTVQLITQLMPFGCLLDYVREHKDNIGSQYLLNWCVQIAKGMNYLEDRRLVHRDLAARNVLVKTPQHVKITDFGLAKLLGAEEKEYHAEGGKVPIKWMALESILHRIYTHQSDVWSYGVTVWELMTFGSKPYDGIPASEISSILEKGERLPQPPICTIDVYMIMVKCWMIDADSRPKFRELIIEFSKMARDPQRYLVIQGDERMHLPSPTDSNFYRALMDEEDMDDVVDADEYLIPQQGFFSSPSTSRTPLLSSLSATSNNSTVACIDRNGLQSCPIKEDSFLQRYSSDPTGALTEDSIDDTFLPVPEYINQSVPKRPAGSVQNPVYHNQPLNPAPSRDPHYQDPHSTAVGNPEYLNTVQPTCVNSTFDSPAHWAQKGSHQISLDNPDYQQDFFPKEAKPNGIFKGSTAENAEYLRVAPQSSEFIGA
bundle/models/generator/egfr_generator.chkpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b90fc1448a02e5160c784064f5ff8ff846ff56ec2ff50a489d6606342446ade9
3
+ size 23227413
bundle/models/generator/reinvent.prior ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b6513ec6dbc54c87ea45cdbf9b4aaefadd7652548b74175366b27f12ec5732fe
3
+ size 23226277
bundle/services/deeppurpose/serve_affinity.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
+ from fastapi.exceptions import RequestValidationError
3
+ from fastapi.responses import JSONResponse
4
+ from pydantic import BaseModel
5
+ from typing import List
6
+ from pathlib import Path
7
+ from collections import OrderedDict
8
+ from rdkit import Chem
9
+ from DeepPurpose.utils import data_process
10
+ from DeepPurpose import DTI as models
11
+ import traceback
12
+ import csv
13
+ import torch
14
+
15
+ # --------------------------------------------------
16
+ # Project paths (absolute, derived from this file)
17
+ # --------------------------------------------------
18
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
19
+
20
+ MODEL_DIR = PROJECT_ROOT / "models" / "affinity"
21
+ TARGET_SEQUENCE_FILE = PROJECT_ROOT / "models" / "affinity" / "target_sequence.txt"
22
+
23
+ app = FastAPI(title="EGFR DeepPurpose REINVENT Adapter")
24
+
25
+ # ---------- Request models ----------
26
+ class PredictRequest(BaseModel):
27
+ smiles: List[str]
28
+
29
+ # ---------- Helpers ----------
30
+ def load_default_egfr_sequence(sequence_path: Path) -> str:
31
+ if not sequence_path.exists():
32
+ raise FileNotFoundError(f"Target sequence file not found: {sequence_path}")
33
+
34
+ seq = sequence_path.read_text(encoding="utf-8").strip()
35
+ if not seq:
36
+ raise RuntimeError(f"Target sequence file is empty: {sequence_path}")
37
+
38
+ return seq
39
+
40
+ def canon(smi: str) -> str:
41
+ try:
42
+ m = Chem.MolFromSmiles(smi)
43
+ return Chem.MolToSmiles(m, isomericSmiles=True) if m is not None else smi
44
+ except Exception:
45
+ return smi
46
+
47
+ # ---------- Load model and target ----------
48
+ seq = load_default_egfr_sequence(TARGET_SEQUENCE_FILE)
49
+ model = models.model_pretrained(path_dir=str(MODEL_DIR))
50
+
51
+ # ---------- Simple cache ----------
52
+ CACHE = OrderedDict()
53
+ CACHE_MAX = 50000
54
+
55
+ def cache_get(k: str):
56
+ if k in CACHE:
57
+ CACHE.move_to_end(k)
58
+ return CACHE[k]
59
+ return None
60
+
61
+ def cache_put(k: str, v: float):
62
+ CACHE[k] = v
63
+ CACHE.move_to_end(k)
64
+ if len(CACHE) > CACHE_MAX:
65
+ CACHE.popitem(last=False)
66
+
67
+ # ---------- Shared prediction core ----------
68
+ def predict_smiles_list(smiles_in: List[str]) -> List[float]:
69
+ if not smiles_in:
70
+ return []
71
+
72
+ out = [None] * len(smiles_in)
73
+ to_compute = []
74
+ idx_map = []
75
+
76
+ for i, smi in enumerate(smiles_in):
77
+ k = canon(smi)
78
+ v = cache_get(k)
79
+ if v is None:
80
+ to_compute.append(smi)
81
+ idx_map.append((i, k))
82
+ else:
83
+ out[i] = float(v)
84
+
85
+ if to_compute:
86
+ X_target = [seq] * len(to_compute)
87
+ y_dummy = [0.0] * len(to_compute)
88
+
89
+ ret = data_process(
90
+ to_compute,
91
+ X_target,
92
+ y_dummy,
93
+ drug_encoding="Morgan",
94
+ target_encoding="AAC",
95
+ split_method="no_split",
96
+ )
97
+
98
+ X_pred = ret[0] if isinstance(ret, (tuple, list)) else ret
99
+
100
+ with torch.inference_mode():
101
+ preds = model.predict(X_pred)
102
+
103
+ for j, p in enumerate(preds):
104
+ i, k = idx_map[j]
105
+ val = float(p)
106
+ out[i] = val
107
+ cache_put(k, val)
108
+
109
+ return [0.0 if v is None else float(v) for v in out]
110
+
111
+ # ---------- Helpful debugging ----------
112
+ @app.exception_handler(RequestValidationError)
113
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
114
+ body = await request.body()
115
+ print("\n=== 422 VALIDATION ERROR ===")
116
+ print("PATH:", request.url.path)
117
+ print("ERRORS:", exc.errors())
118
+ print("BODY:", body.decode("utf-8", errors="replace"))
119
+ print("=== END 422 ===\n")
120
+ return JSONResponse(status_code=422, content={"detail": exc.errors()})
121
+
122
+ # ---------- Legacy endpoint ----------
123
+ @app.post("/predict")
124
+ def predict(req: PredictRequest):
125
+ try:
126
+ preds = predict_smiles_list(req.smiles or [])
127
+ return {"pred_pAff_mean": preds}
128
+ except Exception as e:
129
+ traceback.print_exc()
130
+ raise HTTPException(status_code=500, detail=str(e))
131
+
132
+ # ---------- REINVENT-compatible endpoint ----------
133
+ @app.post("/reinvent_predict")
134
+ async def reinvent_predict(request: Request):
135
+ try:
136
+ payload = await request.json()
137
+
138
+ # REINVENT in your logs sends {"smiles": [...]}
139
+ if isinstance(payload, dict) and "smiles" in payload:
140
+ smiles = payload.get("smiles") or []
141
+ if not isinstance(smiles, list):
142
+ raise HTTPException(status_code=422, detail="Field 'smiles' must be a list.")
143
+ preds = predict_smiles_list(smiles)
144
+ return {"pred_pAff_mean": preds}
145
+
146
+ # Optional compatibility with list-of-items payload
147
+ if isinstance(payload, list):
148
+ smiles = []
149
+ query_ids = []
150
+ for item in payload:
151
+ if not isinstance(item, dict):
152
+ raise HTTPException(status_code=422, detail="Each list item must be an object.")
153
+ if "input_string" not in item or "query_id" not in item:
154
+ raise HTTPException(
155
+ status_code=422,
156
+ detail="Each item must contain 'input_string' and 'query_id'."
157
+ )
158
+ smiles.append(item["input_string"])
159
+ query_ids.append(str(item["query_id"]))
160
+
161
+ preds = predict_smiles_list(smiles)
162
+ successes = [
163
+ {"query_id": qid, "output_value": float(pred)}
164
+ for qid, pred in zip(query_ids, preds)
165
+ ]
166
+ return {"output": {"successes_list": successes}}
167
+
168
+ raise HTTPException(
169
+ status_code=422,
170
+ detail="Unsupported request body. Expected either {'smiles': [...]} or a list of {'input_string','query_id'} items."
171
+ )
172
+
173
+ except HTTPException:
174
+ raise
175
+ except Exception as e:
176
+ traceback.print_exc()
177
+ raise HTTPException(status_code=500, detail=str(e))
178
+
179
+ # ---------- Health ----------
180
+ @app.get("/health")
181
+ def health():
182
+ return {
183
+ "status": "ok",
184
+ "model_dir": str(MODEL_DIR),
185
+ "target_sequence_file": str(TARGET_SEQUENCE_FILE),
186
+ "seq_len": len(seq),
187
+ }
bundle/tools/dock_enriched.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ import pandas as pd
11
+ from rdkit import Chem
12
+ from rdkit.Chem import AllChem
13
+
14
+
15
+ ENERGY_RE = re.compile(
16
+ r"Estimated Free Energy of Binding\s*=\s*([-+]?\d+(?:\.\d+)?)\s*kcal/mol",
17
+ re.IGNORECASE,
18
+ )
19
+
20
+
21
+ def parse_best_binding_energy(dlg_path: Path):
22
+ if not dlg_path.exists():
23
+ return None
24
+
25
+ text = dlg_path.read_text(errors="ignore")
26
+ values = [float(x) for x in ENERGY_RE.findall(text)]
27
+
28
+ if not values:
29
+ return None
30
+
31
+ # More negative is better.
32
+ return min(values)
33
+
34
+
35
+ def make_3d_sdf(smiles: str, sdf_path: Path, seed: int = 42):
36
+ mol = Chem.MolFromSmiles(str(smiles))
37
+ if mol is None:
38
+ raise ValueError(f"Invalid SMILES: {smiles}")
39
+
40
+ mol = Chem.AddHs(mol)
41
+
42
+ params = AllChem.ETKDGv3()
43
+ params.randomSeed = seed
44
+
45
+ status = AllChem.EmbedMolecule(mol, params)
46
+ if status != 0:
47
+ raise RuntimeError(f"3D embedding failed for SMILES: {smiles}")
48
+
49
+ try:
50
+ AllChem.UFFOptimizeMolecule(mol, maxIters=500)
51
+ except Exception:
52
+ pass
53
+
54
+ sdf_path.parent.mkdir(parents=True, exist_ok=True)
55
+ writer = Chem.SDWriter(str(sdf_path))
56
+ writer.write(mol)
57
+ writer.close()
58
+
59
+
60
+ def run_cmd(cmd, cwd=None):
61
+ return subprocess.run(
62
+ cmd,
63
+ cwd=str(cwd) if cwd else None,
64
+ text=True,
65
+ stdout=subprocess.PIPE,
66
+ stderr=subprocess.STDOUT,
67
+ check=False,
68
+ )
69
+
70
+
71
+ def dock_one(smiles, ligand_id, work_dir, adgpu_bin, grid_file, nrun):
72
+ work_dir.mkdir(parents=True, exist_ok=True)
73
+
74
+ sdf_path = work_dir / f"{ligand_id}.sdf"
75
+ pdbqt_path = work_dir / f"{ligand_id}.pdbqt"
76
+
77
+ make_3d_sdf(smiles, sdf_path)
78
+
79
+ mk_prepare = shutil.which("mk_prepare_ligand.py")
80
+ if mk_prepare is None:
81
+ raise RuntimeError("mk_prepare_ligand.py was not found in PATH.")
82
+
83
+ prep = run_cmd([
84
+ mk_prepare,
85
+ "-i", str(sdf_path),
86
+ "-o", str(pdbqt_path),
87
+ ])
88
+
89
+ if prep.returncode != 0 or not pdbqt_path.exists():
90
+ raise RuntimeError(f"Meeko ligand preparation failed:\n{prep.stdout}")
91
+
92
+ dock = run_cmd([
93
+ adgpu_bin,
94
+ "--ffile", str(grid_file.resolve()),
95
+ "--lfile", str(pdbqt_path.resolve()),
96
+ "--nrun", str(nrun),
97
+ ], cwd=work_dir)
98
+
99
+ if dock.returncode != 0:
100
+ raise RuntimeError(f"AutoDock-GPU failed:\n{dock.stdout}")
101
+
102
+ candidates = sorted(work_dir.glob("*.dlg"))
103
+ if not candidates:
104
+ raise RuntimeError(f"No DLG file was produced in {work_dir}")
105
+
106
+ dlg_path = candidates[0]
107
+ score = parse_best_binding_energy(dlg_path)
108
+
109
+ if score is None:
110
+ raise RuntimeError(f"Could not parse binding energy from {dlg_path}")
111
+
112
+ xml_candidates = sorted(work_dir.glob("*.xml"))
113
+ result_file = xml_candidates[0] if xml_candidates else dlg_path
114
+
115
+ return score, str(result_file)
116
+
117
+
118
+ def main():
119
+ parser = argparse.ArgumentParser()
120
+ parser.add_argument("--input", required=True, help="Input enriched CSV.")
121
+ parser.add_argument("--output", required=True, help="Output CSV with docking columns.")
122
+ parser.add_argument("--docking-mode", choices=["off", "top_k", "all"], default="off")
123
+ parser.add_argument("--dock-top-k", type=int, default=10)
124
+ parser.add_argument("--nrun", type=int, default=8)
125
+ parser.add_argument("--adgpu-bin", default=os.environ.get("ADGPU_BIN"))
126
+ parser.add_argument("--grid-file", default="docking/maps_current/4WKQ_receptor_v5_SBr.maps.fld")
127
+ parser.add_argument("--work-dir", default="outputs/docking")
128
+ args = parser.parse_args()
129
+
130
+ df = pd.read_csv(args.input)
131
+
132
+ # Ensure stable dtypes for docking columns.
133
+ if "docking_score" not in df.columns:
134
+ df["docking_score"] = None
135
+ if "docking_status" not in df.columns:
136
+ df["docking_status"] = "not_run"
137
+ if "docking_pose_file" not in df.columns:
138
+ df["docking_pose_file"] = None
139
+
140
+ df["docking_status"] = df["docking_status"].astype("object")
141
+ df["docking_pose_file"] = df["docking_pose_file"].astype("object")
142
+
143
+ if args.docking_mode == "off":
144
+ df["docking_score"] = None
145
+ df["docking_status"] = "not_run"
146
+ df["docking_pose_file"] = None
147
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
148
+ df.to_csv(args.output, index=False)
149
+ print(f"Docking mode off. Wrote: {args.output}")
150
+ return
151
+
152
+ if not args.adgpu_bin:
153
+ raise RuntimeError("Set ADGPU_BIN or pass --adgpu-bin.")
154
+
155
+ adgpu_bin = Path(args.adgpu_bin)
156
+ if not adgpu_bin.exists():
157
+ raise FileNotFoundError(f"AutoDock-GPU binary not found: {adgpu_bin}")
158
+
159
+ grid_file = Path(args.grid_file)
160
+ if not grid_file.exists():
161
+ raise FileNotFoundError(f"Grid file not found: {grid_file}")
162
+
163
+ if "canonical_smiles" not in df.columns:
164
+ raise ValueError("Input CSV must contain canonical_smiles column.")
165
+
166
+ if args.docking_mode == "all":
167
+ indices = list(df.index)
168
+ else:
169
+ indices = list(df.index[: args.dock_top_k])
170
+
171
+ work_root = Path(args.work_dir)
172
+ work_root.mkdir(parents=True, exist_ok=True)
173
+
174
+ for count, idx in enumerate(indices, start=1):
175
+ smiles = df.at[idx, "canonical_smiles"]
176
+ ligand_id = f"ligand_{count:04d}"
177
+
178
+ try:
179
+ score, result_file = dock_one(
180
+ smiles=smiles,
181
+ ligand_id=ligand_id,
182
+ work_dir=work_root / ligand_id,
183
+ adgpu_bin=str(adgpu_bin),
184
+ grid_file=grid_file,
185
+ nrun=args.nrun,
186
+ )
187
+ df.at[idx, "docking_score"] = score
188
+ df.at[idx, "docking_status"] = "completed"
189
+ df.at[idx, "docking_pose_file"] = result_file
190
+ print(f"[OK] {ligand_id}: {score:.2f} kcal/mol")
191
+
192
+ except Exception as e:
193
+ df.at[idx, "docking_score"] = None
194
+ df.at[idx, "docking_status"] = f"failed: {str(e)[:160]}"
195
+ df.at[idx, "docking_pose_file"] = None
196
+ print(f"[FAILED] {ligand_id}: {e}")
197
+
198
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
199
+ df.to_csv(args.output, index=False)
200
+ print(f"Wrote: {args.output}")
201
+
202
+
203
+ if __name__ == "__main__":
204
+ main()
bundle/tools/enrich_generated.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ import requests
9
+ import os
10
+ import sys
11
+
12
+ from rdkit import Chem
13
+ from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED
14
+
15
+
16
+ def canonicalize(smiles: str):
17
+ mol = Chem.MolFromSmiles(str(smiles))
18
+ if mol is None:
19
+ return None, None
20
+ can = Chem.MolToSmiles(mol, isomericSmiles=True)
21
+ return mol, can
22
+
23
+
24
+ def try_sa_score(mol):
25
+ """
26
+ Compute RDKit SA score.
27
+
28
+ RDKit Contrib is installed in different locations depending on the package source.
29
+ Common conda location:
30
+ $CONDA_PREFIX/share/RDKit/Contrib/SA_Score/sascorer.py
31
+
32
+ Lower score means easier synthesis.
33
+ Higher score means harder synthesis.
34
+ """
35
+ import_paths = []
36
+
37
+ # First try the normal Python import.
38
+ try:
39
+ from rdkit.Contrib.SA_Score import sascorer
40
+ return float(sascorer.calculateScore(mol))
41
+ except Exception:
42
+ pass
43
+
44
+ # Then search common RDKit Contrib locations.
45
+ conda_prefix = os.environ.get("CONDA_PREFIX")
46
+ if conda_prefix:
47
+ import_paths.append(Path(conda_prefix) / "share" / "RDKit" / "Contrib" / "SA_Score")
48
+
49
+ import_paths.extend([
50
+ Path("/opt/conda/envs/ailixir/share/RDKit/Contrib/SA_Score"),
51
+ Path("/opt/conda/share/RDKit/Contrib/SA_Score"),
52
+ Path("/usr/share/RDKit/Contrib/SA_Score"),
53
+ Path("/usr/local/share/RDKit/Contrib/SA_Score"),
54
+ ])
55
+
56
+ for p in import_paths:
57
+ sascorer_file = p / "sascorer.py"
58
+ fpscores_file = p / "fpscores.pkl.gz"
59
+
60
+ if sascorer_file.exists() and fpscores_file.exists():
61
+ sys.path.insert(0, str(p))
62
+ try:
63
+ import sascorer
64
+ return float(sascorer.calculateScore(mol))
65
+ except Exception:
66
+ continue
67
+
68
+ return None
69
+
70
+
71
+ def compute_properties(smiles: str):
72
+ mol, can = canonicalize(smiles)
73
+
74
+ if mol is None:
75
+ return {
76
+ "valid": False,
77
+ "canonical_smiles": None,
78
+ "mw": None,
79
+ "logp": None,
80
+ "tpsa": None,
81
+ "hbd": None,
82
+ "hba": None,
83
+ "rot_bonds": None,
84
+ "qed": None,
85
+ "sa_score": None,
86
+ }
87
+
88
+ return {
89
+ "valid": True,
90
+ "canonical_smiles": can,
91
+ "mw": float(Descriptors.MolWt(mol)),
92
+ "logp": float(Crippen.MolLogP(mol)),
93
+ "tpsa": float(rdMolDescriptors.CalcTPSA(mol)),
94
+ "hbd": int(Lipinski.NumHDonors(mol)),
95
+ "hba": int(Lipinski.NumHAcceptors(mol)),
96
+ "rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
97
+ "qed": float(QED.qed(mol)),
98
+ "sa_score": try_sa_score(mol),
99
+ }
100
+
101
+
102
+ def call_affinity_api(smiles_list, url: str):
103
+ if not smiles_list:
104
+ return []
105
+
106
+ try:
107
+ response = requests.post(
108
+ url,
109
+ json={"smiles": smiles_list},
110
+ timeout=300,
111
+ )
112
+ response.raise_for_status()
113
+
114
+ payload = response.json()
115
+ preds = payload.get("pred_pAff_mean")
116
+
117
+ if preds is None:
118
+ raise RuntimeError(
119
+ f"Affinity API response missing 'pred_pAff_mean'. Response was: {json.dumps(payload)[:500]}"
120
+ )
121
+
122
+ return [float(x) for x in preds]
123
+
124
+ except Exception as e:
125
+ print(f"Warning: Affinity API call failed ({e}). Returning null predictions.")
126
+ return [None] * len(smiles_list)
127
+
128
+
129
+ def main():
130
+ parser = argparse.ArgumentParser()
131
+ parser.add_argument("--input", required=True, help="Input generated_smiles.csv from REINVENT sampling.")
132
+ parser.add_argument("--output", required=True, help="Output enriched CSV.")
133
+ parser.add_argument(
134
+ "--affinity-url",
135
+ default="http://127.0.0.1:8001/reinvent_predict",
136
+ help="DeepPurpose/FastAPI affinity endpoint.",
137
+ )
138
+ parser.add_argument(
139
+ "--top-k",
140
+ type=int,
141
+ default=0,
142
+ help="If >0, keep only top K rows after sorting by pred_pAff_mean desc then QED desc.",
143
+ )
144
+ args = parser.parse_args()
145
+
146
+ input_path = Path(args.input)
147
+ output_path = Path(args.output)
148
+ output_path.parent.mkdir(parents=True, exist_ok=True)
149
+
150
+ df = pd.read_csv(input_path)
151
+
152
+ if "SMILES" not in df.columns:
153
+ raise ValueError(f"Input file must contain a SMILES column. Found columns: {list(df.columns)}")
154
+
155
+ props = [compute_properties(smi) for smi in df["SMILES"].tolist()]
156
+ props_df = pd.DataFrame(props)
157
+
158
+ out = pd.concat([df.reset_index(drop=True), props_df.reset_index(drop=True)], axis=1)
159
+
160
+ valid_mask = out["valid"] == True
161
+ valid_smiles = out.loc[valid_mask, "canonical_smiles"].tolist()
162
+
163
+ pred_values = call_affinity_api(valid_smiles, args.affinity_url)
164
+
165
+ out["pred_pAff_mean"] = None
166
+ out.loc[valid_mask, "pred_pAff_mean"] = pred_values
167
+
168
+ # Docking is optional in v1. Keep stable columns for frontend/backend filters.
169
+ out["docking_score"] = None
170
+ out["docking_status"] = "not_run"
171
+ out["docking_pose_file"] = None
172
+
173
+ # Sort for display only.
174
+ # This is not a final scientific ranking.
175
+ out = out.sort_values(
176
+ by=["pred_pAff_mean", "qed"],
177
+ ascending=[False, False],
178
+ )
179
+
180
+ if args.top_k and args.top_k > 0:
181
+ out = out.head(args.top_k)
182
+
183
+ out.to_csv(output_path, index=False)
184
+
185
+ print(f"Wrote: {output_path}")
186
+ print(f"Rows: {len(out)}")
187
+ print("Columns:", ", ".join(out.columns))
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
bundle_files.txt ADDED
Binary file (15.3 kB). View file
 
start_hf.sh ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ mkdir -p /app/outputs/jobs
4
+ mkdir -p /app/outputs/deeppurpose
5
+ mkdir -p /home/abdullah/projects/egfr_drug_discovery/runs/deeppurpose
6
+
7
+ export PORT="${PORT:-7860}"
8
+ export PUBLIC_BASE_URL="${PUBLIC_BASE_URL:-http://localhost:${PORT}}"
9
+ export DEEPPURPOSE_URL="${DEEPPURPOSE_URL:-http://127.0.0.1:8001/reinvent_predict}"
10
+ export REINVENT_DEVICE="${REINVENT_DEVICE:-cpu}"
11
+ export PYTHON="${PYTHON:-python}"
12
+
13
+ if [ -x "/opt/AutoDock-GPU/bin/autodock_cpu_1wi" ]; then
14
+ export ADGPU_BIN="${ADGPU_BIN:-/opt/AutoDock-GPU/bin/autodock_cpu_1wi}"
15
+ elif [ -x "/opt/AutoDock-GPU/bin/autodock_gpu_64wi" ]; then
16
+ export ADGPU_BIN="${ADGPU_BIN:-/opt/AutoDock-GPU/bin/autodock_gpu_64wi}"
17
+ else
18
+ export ADGPU_BIN="${ADGPU_BIN:-}"
19
+ fi
20
+
21
+ echo "PORT=${PORT}"
22
+ echo "PUBLIC_BASE_URL=${PUBLIC_BASE_URL}"
23
+ echo "DEEPPURPOSE_URL=${DEEPPURPOSE_URL}"
24
+ echo "REINVENT_DEVICE=${REINVENT_DEVICE}"
25
+ echo "ADGPU_BIN=${ADGPU_BIN}"
26
+
27
+ echo "Starting DeepPurpose affinity service on 127.0.0.1:8001..."
28
+ conda run --no-capture-output -n dp \
29
+ uvicorn services.deeppurpose.serve_affinity:app \
30
+ --app-dir /app/bundle \
31
+ --host 127.0.0.1 \
32
+ --port 8001 &
33
+
34
+ AFFINITY_PID=$!
35
+
36
+ echo "Waiting for DeepPurpose affinity service..."
37
+ conda run --no-capture-output -n ailixir python - <<'PY'
38
+ import time
39
+ import sys
40
+ import requests
41
+
42
+ url = "http://127.0.0.1:8001/health"
43
+
44
+ for i in range(180):
45
+ try:
46
+ r = requests.get(url, timeout=3)
47
+ print(f"Affinity health attempt {i+1}: {r.status_code}")
48
+ if r.status_code == 200:
49
+ print("Affinity service is ready.")
50
+ sys.exit(0)
51
+ except Exception as e:
52
+ print(f"Affinity health attempt {i+1} failed: {e}")
53
+ time.sleep(2)
54
+
55
+ print("Affinity service did not become ready.")
56
+ sys.exit(1)
57
+ PY
58
+
59
+ echo "Starting Generation API on 0.0.0.0:${PORT}..."
60
+ exec conda run --no-capture-output -n ailixir \
61
+ uvicorn api:app \
62
+ --host 0.0.0.0 \
63
+ --port "${PORT}"