Spaces:
Runtime error
Runtime error
Commit ·
89d9c28
0
Parent(s):
Deploy AILIXIR generation API Docker Space
Browse files- .dockerignore +18 -0
- .gitattributes +4 -0
- .gitignore +17 -0
- Dockerfile +142 -0
- README.md +19 -0
- api.py +675 -0
- bundle/.gitattributes +62 -0
- bundle/README.md +18 -0
- bundle/configs/backend_contract.json +80 -0
- bundle/configs/sampling_model.bundle.toml +16 -0
- bundle/docking/maps_current/4WKQ_clean.pdb +0 -0
- bundle/docking/maps_current/4WKQ_raw.pdb +0 -0
- bundle/docking/maps_current/4WKQ_receptor.A.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.Br.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.C.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.Cl.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.F.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.HD.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.N.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.NA.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.OA.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.S.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.SA.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.d.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor.e.map +3 -0
- bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.fld +52 -0
- bundle/docking/maps_current/4WKQ_receptor_v5_SBr.maps.xyz +3 -0
- bundle/docking/maps_current/autogrid_v9.log +0 -0
- bundle/docking/maps_current/ligand.sdf +217 -0
- bundle/docking/maps_current/receptorH.gpf +22 -0
- bundle/docking/maps_current/receptorH_flex.pdbqt +13 -0
- bundle/docking/maps_current/receptorH_rigid.pdbqt +0 -0
- bundle/docs/API_RESPONSE_SPEC.md +111 -0
- bundle/examples/generate_response.example.json +131 -0
- bundle/models/affinity/config.pkl +3 -0
- bundle/models/affinity/model.pt +3 -0
- bundle/models/affinity/target_sequence.txt +1 -0
- bundle/models/generator/egfr_generator.chkpt +3 -0
- bundle/models/generator/reinvent.prior +3 -0
- bundle/services/deeppurpose/serve_affinity.py +187 -0
- bundle/tools/dock_enriched.py +204 -0
- bundle/tools/enrich_generated.py +191 -0
- bundle_files.txt +0 -0
- 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}"
|