Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +25 -10
- README.md +387 -0
- configs/prop/pdbbind_general_egnn.yml +39 -0
- configs/prop/pdbbind_general_egnn_enc_final_h.yml +44 -0
- configs/sampling.yml +10 -0
- configs/training.yml +64 -0
- examples/3ug2_ligand.sdf +136 -0
- examples/3ug2_protein.pdb +0 -0
- models/__init__.py +2 -0
- models/__pycache__/__init__.cpython-311.pyc +0 -0
- models/__pycache__/common.cpython-311.pyc +0 -0
- models/__pycache__/egnn.cpython-311.pyc +0 -0
- models/__pycache__/molopt_score_model.cpython-311.pyc +0 -0
- models/__pycache__/uni_transformer.cpython-311.pyc +0 -0
- models/common.py +212 -0
- models/egnn.py +133 -0
- models/molopt_score_model.py +708 -0
- models/property_pred/__init__.py +2 -0
- models/property_pred/__pycache__/__init__.cpython-311.pyc +0 -0
- models/property_pred/__pycache__/prop_egnn.cpython-311.pyc +0 -0
- models/property_pred/__pycache__/prop_model.cpython-311.pyc +0 -0
- models/property_pred/prop_egnn.py +83 -0
- models/property_pred/prop_model.py +212 -0
- models/uni_transformer.py +328 -0
- scripts/__init__.py +2 -0
- scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- scripts/__pycache__/sample_diffusion.cpython-311.pyc +0 -0
- scripts/__pycache__/sample_for_pocket.cpython-311.pyc +0 -0
- scripts/batch_sample_diffusion.sh +21 -0
- scripts/data_preparation/__init__.py +2 -0
- scripts/data_preparation/clean_crossdocked.py +59 -0
- scripts/data_preparation/extract_pockets.py +76 -0
- scripts/data_preparation/split_pl_dataset.py +112 -0
- scripts/dock_baseline.py +59 -0
- scripts/dock_testset.py +76 -0
- scripts/evaluate_diffusion.py +208 -0
- scripts/evaluate_from_meta.py +138 -0
- scripts/inference.sh +27 -0
- scripts/likelihood_est_diffusion.py +198 -0
- scripts/property_prediction/__init__.py +2 -0
- scripts/property_prediction/__pycache__/__init__.cpython-311.pyc +0 -0
- scripts/property_prediction/__pycache__/local_misc_prop.cpython-311.pyc +0 -0
- scripts/property_prediction/eval_prop.py +93 -0
- scripts/property_prediction/extract_pockets.py +121 -0
- scripts/property_prediction/fixed_inference.py +225 -0
- scripts/property_prediction/inference.py +124 -0
- scripts/property_prediction/local_misc_prop.py +64 -0
- scripts/property_prediction/pdbbind_split.py +66 -0
- scripts/property_prediction/train_prop.py +229 -0
- scripts/sample_diffusion.py +188 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,50 @@
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 4 |
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
*.bin 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 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 11 |
*.model filter=lfs diff=lfs merge=lfs -text
|
| 12 |
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
| 13 |
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 16 |
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
|
|
|
|
|
|
|
| 18 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 19 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 20 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 21 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 22 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 24 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.db* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.ark* filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
**/*ckpt*data* filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
**/*ckpt*.meta filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
**/*ckpt*.index filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*.gguf* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.ggml filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.llamafile* filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.pt2 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
|
| 44 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 46 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 47 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
|
| 49 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
frameworks:
|
| 3 |
+
- ""
|
| 4 |
+
language:
|
| 5 |
+
- en
|
| 6 |
+
license: mit
|
| 7 |
+
tags:
|
| 8 |
+
- OneScience
|
| 9 |
+
- affinity prediction
|
| 10 |
+
- molecule generation
|
| 11 |
+
---
|
| 12 |
+
<p align="center">
|
| 13 |
+
<strong>
|
| 14 |
+
<span style="font-size: 30px;">TargetDiff</span>
|
| 15 |
+
</strong>
|
| 16 |
+
</p>
|
| 17 |
+
|
| 18 |
+
# Model Overview
|
| 19 |
+
|
| 20 |
+
TargetDiff is a bioinformatics model for target-aware molecule generation and protein-ligand affinity prediction.
|
| 21 |
+
|
| 22 |
+
The original paper is _3D Equivariant Diffusion for Target-Aware Molecule Generation and Affinity Prediction_ (ICLR 2023).
|
| 23 |
+
|
| 24 |
+
# Model Description
|
| 25 |
+
|
| 26 |
+
TargetDiff is based on a 3D equivariant diffusion network. Given a protein binding pocket, it generates candidate small molecules and can use an EGNN property prediction branch to predict protein-ligand complex affinity.
|
| 27 |
+
|
| 28 |
+
The current package organizes TargetDiff example scripts, a snapshot of the model implementation, sample inputs, and pretrained weights in the same directory. Datasets will be uploaded later.
|
| 29 |
+
|
| 30 |
+
# Use Cases
|
| 31 |
+
|
| 32 |
+
| Use case | Description |
|
| 33 |
+
| :---: | :---: |
|
| 34 |
+
| Protein-ligand affinity prediction | Takes a protein PDB file and ligand SDF file as input, and outputs molar concentration predictions for `Ki`, `Kd`, or `IC50` |
|
| 35 |
+
| Target-aware small molecule generation | Takes a protein binding pocket PDB file as input, and outputs generated molecule `sample.pt` files and SDF files for molecules that can be reconstructed |
|
| 36 |
+
| Diffusion model training | Trains the TargetDiff molecule generation model using CrossDocked2020 pocket data |
|
| 37 |
+
| Property prediction training | Trains an EGNN affinity prediction model using PDBbind data |
|
| 38 |
+
| Generated result evaluation | Computes metrics such as stability, reconstruction success rate, QED, SA, and optional Vina docking metrics for sampling results |
|
| 39 |
+
|
| 40 |
+
# Usage
|
| 41 |
+
|
| 42 |
+
## 1. Using OneCode
|
| 43 |
+
|
| 44 |
+
You can try intelligent one-click AI4S programming through the OneCode online environment:
|
| 45 |
+
|
| 46 |
+
[Try intelligent one-click AI4S programming](https://web-2069360198568017922-iaaj.ksai.scnet.cn:58043/home)
|
| 47 |
+
|
| 48 |
+
## 2. Manual Installation and Usage
|
| 49 |
+
|
| 50 |
+
**Hardware Requirements**
|
| 51 |
+
|
| 52 |
+
- Running on a GPU or DCU is recommended.
|
| 53 |
+
- CPU can be used for connectivity checks, but it is relatively slow.
|
| 54 |
+
- DCU users need to install DTK in advance. DTK 25.04.2 or later is recommended, or the OneScience-recommended version that matches the current cluster.
|
| 55 |
+
|
| 56 |
+
**Software Requirements**
|
| 57 |
+
|
| 58 |
+
First enter an environment where the OneScience bioinformatics dependencies have been installed.
|
| 59 |
+
|
| 60 |
+
**Environment Checks**
|
| 61 |
+
|
| 62 |
+
- NVIDIA GPU:
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
nvidia-smi
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
- Hygon DCU:
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
hy-smi
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## Quick Start
|
| 75 |
+
|
| 76 |
+
### 1. Install the Runtime Environment
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
conda create -n onescience311 python=3.11 -y
|
| 80 |
+
conda activate onescience311
|
| 81 |
+
pip install onescience[bio] -i http://mirrors.onescience.ai:3141/pypi/simple/ --trusted-host mirrors.onescience.ai
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
If the following code cannot find required libraries at runtime, activate CUDA as shown below.
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
source ${ROCM_PATH}/cuda/env.sh
|
| 88 |
+
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH"
|
| 89 |
+
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/python3.11/site-packages/fastpt/torch/lib:$LD_LIBRARY_PATH"
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### 2. Download the Model
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
hf download --model OneScience-Sugon/targetdiff --local-dir ./targetdiff
|
| 96 |
+
cd targetdiff
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
### Training Weights and Datasets
|
| 100 |
+
|
| 101 |
+
Training weights are already included in the `weights` folder and can be used directly after downloading the model package.
|
| 102 |
+
Datasets will be uploaded to ModelScope soon, and command-line downloads will be supported later.
|
| 103 |
+
|
| 104 |
+
### 3. Affinity Prediction
|
| 105 |
+
|
| 106 |
+
#### 3.1 Affinity Prediction Training
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
bash scripts/train_prop.sh
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
This script automatically performs the following steps:
|
| 113 |
+
|
| 114 |
+
1. Extract binding pockets from the PDBbind refined set.
|
| 115 |
+
2. Split the training, validation, and test sets according to the coreset.
|
| 116 |
+
3. Train an EGNN-based protein-ligand binding affinity prediction model.
|
| 117 |
+
|
| 118 |
+
#### 3.2 Affinity Prediction Evaluation
|
| 119 |
+
|
| 120 |
+
Use the trained affinity prediction model to evaluate on the test set. The expected official metrics on PDBBind v2016 are:
|
| 121 |
+
|
| 122 |
+
| RMSE | MAE | R² | Pearson | Spearman |
|
| 123 |
+
|------|-----|----|---------|----------|
|
| 124 |
+
| 1.316 | 1.031 | 0.633 | 0.797 | 0.782 |
|
| 125 |
+
|
| 126 |
+
Run:
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 130 |
+
python scripts/property_prediction/eval_prop.py \
|
| 131 |
+
--ckpt_path ${ONESCIENCE_MODELS_DIR}/targetdiff/pretrained_models/egnn_pdbbind_v2016.pt \
|
| 132 |
+
--device cuda
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
#### 3.3 Affinity Prediction Inference
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
bash scripts/inference.sh
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
By default, this script performs affinity prediction on the example protein-ligand pair `3ug2`, using the default weights and example data:
|
| 142 |
+
|
| 143 |
+
- Model weights: `${ONESCIENCE_MODELS_DIR}/targetdiff/pretrained_models/egnn_pdbbind_v2016.pt`
|
| 144 |
+
- Protein: `${ONESCIENCE_DATASETS_DIR}/targetdiff/examples/3ug2_protein.pdb`
|
| 145 |
+
- Ligand: `${ONESCIENCE_DATASETS_DIR}/targetdiff/examples/3ug2_ligand.sdf`
|
| 146 |
+
- Affinity type: default `Kd`
|
| 147 |
+
- Compute device: default `cuda`
|
| 148 |
+
|
| 149 |
+
### 4. Molecular Sampling
|
| 150 |
+
|
| 151 |
+
#### 4.1 Single Test-Set Sample
|
| 152 |
+
|
| 153 |
+
```bash
|
| 154 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 155 |
+
python -m scripts.sample_diffusion configs/sampling.yml -i 0 --batch_size 50 --result_path ./outputs
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
This script reads the diffusion model checkpoint from `configs/sampling.yml`, generates candidate ligand molecules for the `i`-th pocket in the test set, and saves the result as `result_i.pt`.
|
| 159 |
+
|
| 160 |
+
#### 4.2 Multi-GPU Batch Sampling
|
| 161 |
+
|
| 162 |
+
```bash
|
| 163 |
+
bash scripts/batch_sample_diffusion.sh configs/sampling.yml outputs 4 0 0
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Parameter descriptions:
|
| 167 |
+
|
| 168 |
+
| Parameter | Description |
|
| 169 |
+
|------|------|
|
| 170 |
+
| `$1` | Sampling configuration file path |
|
| 171 |
+
| `$2` | Result output directory |
|
| 172 |
+
| `$3` | Total number of worker nodes |
|
| 173 |
+
| `$4` | Current node index, starting from 0 |
|
| 174 |
+
| `$5` | Starting data index |
|
| 175 |
+
|
| 176 |
+
Example of multi-GPU parallel sampling:
|
| 177 |
+
|
| 178 |
+
```bash
|
| 179 |
+
CUDA_VISIBLE_DEVICES=0 bash scripts/batch_sample_diffusion.sh configs/sampling.yml outputs 4 0 0 &
|
| 180 |
+
CUDA_VISIBLE_DEVICES=1 bash scripts/batch_sample_diffusion.sh configs/sampling.yml outputs 4 1 0 &
|
| 181 |
+
CUDA_VISIBLE_DEVICES=2 bash scripts/batch_sample_diffusion.sh configs/sampling.yml outputs 4 2 0 &
|
| 182 |
+
CUDA_VISIBLE_DEVICES=3 bash scripts/batch_sample_diffusion.sh configs/sampling.yml outputs 4 3 0 &
|
| 183 |
+
wait
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
The script internally fixes `TOTAL_TASKS=100` and `BATCH_SIZE=50`, and assigns samples to nodes by index modulo.
|
| 187 |
+
|
| 188 |
+
#### 4.3 Sampling for a Custom PDB Pocket
|
| 189 |
+
|
| 190 |
+
```bash
|
| 191 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 192 |
+
python -m scripts.sample_for_pocket configs/sampling.yml \
|
| 193 |
+
--pdb_path /path/to/pocket.pdb \
|
| 194 |
+
--result_path ./outputs_pdb \
|
| 195 |
+
--num_samples 5 \
|
| 196 |
+
--batch_size 1
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
Parameter descriptions:
|
| 200 |
+
|
| 201 |
+
| Parameter | Required | Description |
|
| 202 |
+
|------|----------|------|
|
| 203 |
+
| `config` | Yes | Sampling configuration file path |
|
| 204 |
+
| `--pdb_path` | Yes | Protein pocket PDB file path. A 10 Å pocket is recommended. |
|
| 205 |
+
| `--result_path` | No | Result output directory, default `./outputs_pdb` |
|
| 206 |
+
| `--num_samples` | No | Number of molecules to sample, read from the configuration file by default |
|
| 207 |
+
| `--batch_size` | No | Batch size, default `100` |
|
| 208 |
+
| `--device` | No | Compute device, default `cuda:0` |
|
| 209 |
+
|
| 210 |
+
Sampling results are saved as `outputs_pdb/sample.pt`, and successfully reconstructed molecules are additionally written to `outputs_pdb/sdf/`.
|
| 211 |
+
|
| 212 |
+
### 5. Generated Molecule Evaluation
|
| 213 |
+
|
| 214 |
+
#### 5.1 Evaluation from Sampling Results
|
| 215 |
+
|
| 216 |
+
If docking evaluation is required (`vina_score` / `vina_dock` / `qvina`), also install:
|
| 217 |
+
|
| 218 |
+
```bash
|
| 219 |
+
pip install meeko==0.1.dev3 scipy pdb2pqr vina==1.2.2
|
| 220 |
+
python -m pip install git+https://github.com/Valdes-Tresanco-MS/AutoDockTools_py3
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
```bash
|
| 224 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 225 |
+
python scripts/evaluate_diffusion.py ./outputs --docking_mode vina_score --protein_root /path/to/protein_root
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
Parameter descriptions:
|
| 229 |
+
|
| 230 |
+
| Parameter | Required | Description |
|
| 231 |
+
|------|----------|------|
|
| 232 |
+
| `sample_path` | Yes | Sampling result directory containing `result_*.pt` files |
|
| 233 |
+
| `--docking_mode` | Yes | Docking mode. Options: `none`, `vina_score`, `vina_dock`, `qvina` |
|
| 234 |
+
| `--protein_root` | No | Root directory of the original protein files, used for docking evaluation |
|
| 235 |
+
| `--eval_step` | No | Which sampling step to evaluate, default `-1` (the final step) |
|
| 236 |
+
| `--eval_num_examples` | No | Number of samples to evaluate, default all |
|
| 237 |
+
| `--exhaustiveness` | No | Docking search intensity, default `16` |
|
| 238 |
+
| `--save` | No | Whether to save evaluation results, default `True` |
|
| 239 |
+
|
| 240 |
+
Supported docking modes:
|
| 241 |
+
|
| 242 |
+
| Mode | Description |
|
| 243 |
+
|------|------|
|
| 244 |
+
| `none` | Computes only metrics such as validity, uniqueness, and novelty, without docking |
|
| 245 |
+
| `vina_score` | Uses AutoDock Vina to score generated molecules |
|
| 246 |
+
| `vina_dock` | Uses AutoDock Vina to redock generated molecules |
|
| 247 |
+
| `qvina` | Uses QuickVina for docking |
|
| 248 |
+
|
| 249 |
+
The first run in `vina_score` or `vina_dock` mode may take some time to prepare `pdbqt` and `pqr` files.
|
| 250 |
+
|
| 251 |
+
#### 5.2 Evaluation from Meta Files
|
| 252 |
+
|
| 253 |
+
The official project provides sampled and docked meta files, including TargetDiff and baselines such as CVAE, AR, and Pocket2Mol. These files can be downloaded and evaluated directly:
|
| 254 |
+
|
| 255 |
+
| Meta file | Corresponding paper |
|
| 256 |
+
|-----------|----------|
|
| 257 |
+
| `crossdocked_test_vina_docked.pt` | Original test-set docking results |
|
| 258 |
+
| `cvae_vina_docked.pt` | liGAN |
|
| 259 |
+
| `ar_vina_docked.pt` | AR |
|
| 260 |
+
| `pocket2mol_vina_docked.pt` | Pocket2Mol |
|
| 261 |
+
| `targetdiff_vina_docked.pt` | TargetDiff |
|
| 262 |
+
|
| 263 |
+
Official meta file download URL: https://drive.google.com/drive/folders/19imu-mlwrjnQhgbXpwsLgA17s1Rv70YS?usp=share_link
|
| 264 |
+
|
| 265 |
+
Evaluation command:
|
| 266 |
+
|
| 267 |
+
```bash
|
| 268 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 269 |
+
python scripts/evaluate_from_meta.py sampling_results/targetdiff_vina_docked.pt --result_path eval_targetdiff
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
Parameter descriptions:
|
| 273 |
+
|
| 274 |
+
| Parameter | Required | Description |
|
| 275 |
+
|------|----------|------|
|
| 276 |
+
| `meta_file` | Yes | `.pt` file containing sampling and docking results |
|
| 277 |
+
| `--result_path` | No | Evaluation result output directory, default `eval_results` |
|
| 278 |
+
|
| 279 |
+
---
|
| 280 |
+
|
| 281 |
+
### 6. Diffusion Model Training
|
| 282 |
+
|
| 283 |
+
```bash
|
| 284 |
+
bash train_diffusion.sh
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
By default, this reads `configs/training.yml`, trains on the CrossDocked2020 pocket dataset, and saves logs and checkpoints to `./logs_diffusion/`.
|
| 288 |
+
|
| 289 |
+
**Custom configuration or parameter overrides:**
|
| 290 |
+
|
| 291 |
+
```bash
|
| 292 |
+
# Specify a custom configuration file
|
| 293 |
+
bash train_diffusion.sh configs/custom_training.yml
|
| 294 |
+
|
| 295 |
+
# Override training parameters from the command line
|
| 296 |
+
bash train_diffusion.sh --train.batch_size 8 --train.max_iters 500000
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
**Main training parameters:**
|
| 300 |
+
|
| 301 |
+
| Parameter | Default value | Description |
|
| 302 |
+
|------|--------|------|
|
| 303 |
+
| `data.path` | `${ONESCIENCE_DATASETS_DIR}/targetdiff/data/crossdocked_v1.1_rmsd1.0_pocket10` | Training data directory |
|
| 304 |
+
| `data.split` | `${ONESCIENCE_DATASETS_DIR}/targetdiff/data/crossdocked_pocket10_pose_split.pt` | Training/validation/test split file |
|
| 305 |
+
| `train.batch_size` | `4` | Batch size |
|
| 306 |
+
| `train.max_iters` | `10000000` | Maximum number of iterations |
|
| 307 |
+
| `train.lr` | `5.e-4` | Learning rate |
|
| 308 |
+
| `logdir` | `./logs_diffusion` | Log output directory |
|
| 309 |
+
|
| 310 |
+
---
|
| 311 |
+
|
| 312 |
+
### 7. Data Preprocessing
|
| 313 |
+
|
| 314 |
+
#### 7.1 CrossDocked2020 Data Preprocessing
|
| 315 |
+
|
| 316 |
+
To process CrossDocked2020 data from scratch, follow these steps:
|
| 317 |
+
|
| 318 |
+
1. Download CrossDocked2020 v1.1 and save it to `data/CrossDocked2020`.
|
| 319 |
+
2. Filter samples with RMSD < 1 Å:
|
| 320 |
+
|
| 321 |
+
```bash
|
| 322 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 323 |
+
python scripts/data_preparation/clean_crossdocked.py \
|
| 324 |
+
--source data/CrossDocked2020 \
|
| 325 |
+
--dest data/crossdocked_v1.1_rmsd1.0 \
|
| 326 |
+
--rmsd_thr 1.0
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
3. Extract 10 Å binding pockets from proteins:
|
| 330 |
+
|
| 331 |
+
```bash
|
| 332 |
+
python scripts/data_preparation/extract_pockets.py \
|
| 333 |
+
--source data/crossdocked_v1.1_rmsd1.0 \
|
| 334 |
+
--dest data/crossdocked_v1.1_rmsd1.0_pocket10
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
4. Split the training and test sets:
|
| 338 |
+
|
| 339 |
+
```bash
|
| 340 |
+
python scripts/data_preparation/split_pl_dataset.py \
|
| 341 |
+
--path data/crossdocked_v1.1_rmsd1.0_pocket10 \
|
| 342 |
+
--dest data/crossdocked_pocket10_pose_split.pt \
|
| 343 |
+
--fixed_split data/split_by_name.pt
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
#### 7.2 PDBbind Data Preprocessing
|
| 347 |
+
|
| 348 |
+
The affinity prediction training script `train_prop.sh` automatically performs pocket extraction and dataset splitting. To run these steps separately, use the following commands:
|
| 349 |
+
|
| 350 |
+
```bash
|
| 351 |
+
export PYTHONPATH=../../../src:$PYTHONPATH
|
| 352 |
+
|
| 353 |
+
python scripts/property_prediction/extract_pockets.py \
|
| 354 |
+
--source data/pdbbind_v2020 \
|
| 355 |
+
--dest data/pdbbind_v2020_processed \
|
| 356 |
+
--subset refined \
|
| 357 |
+
--num_workers 16
|
| 358 |
+
|
| 359 |
+
python scripts/property_prediction/pdbbind_split.py \
|
| 360 |
+
--split_mode coreset \
|
| 361 |
+
--index_path data/pdbbind_v2020_processed/pocket_10_refined/index.pkl \
|
| 362 |
+
--test_path data/pdbbind_v2016/coreset \
|
| 363 |
+
--save_path data/pdbbind_v2020_processed/pocket_10_refined/split.pt
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
---
|
| 367 |
+
|
| 368 |
+
# Official OneScience Information
|
| 369 |
+
|
| 370 |
+
| Platform | OneScience main repository | Skills repository |
|
| 371 |
+
| --- | --- | --- |
|
| 372 |
+
| Gitee | https://gitee.com/onescience-ai/onescience | https://gitee.com/onescience-ai/oneskills |
|
| 373 |
+
| GitHub | https://github.com/onescience-ai/OneScience | https://github.com/onescience-ai/oneskills |
|
| 374 |
+
|
| 375 |
+
# Citation and License
|
| 376 |
+
|
| 377 |
+
- The original TargetDiff code is licensed under the MIT License. This repository retains source attribution and is organized for OneScience ModelScope automated runtime scenarios.
|
| 378 |
+
- If you use TargetDiff results in research, we recommend citing the original TargetDiff paper and relevant OneScience project information. Depending on the actual task, also add citations for datasets or tools such as CrossDocked2020, PDBbind, RDKit, OpenBabel, and Vina/QVina.
|
| 379 |
+
|
| 380 |
+
```bibtex
|
| 381 |
+
@inproceedings{guan3d,
|
| 382 |
+
title={3D Equivariant Diffusion for Target-Aware Molecule Generation and Affinity Prediction},
|
| 383 |
+
author={Guan, Jiaqi and Qian, Wesley Wei and Peng, Xingang and Su, Yufeng and Peng, Jian and Ma, Jianzhu},
|
| 384 |
+
booktitle={International Conference on Learning Representations},
|
| 385 |
+
year={2023}
|
| 386 |
+
}
|
| 387 |
+
```
|
configs/prop/pdbbind_general_egnn.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
hidden_channels: 256
|
| 3 |
+
encoder:
|
| 4 |
+
name: egnn
|
| 5 |
+
num_layers: 6
|
| 6 |
+
hidden_dim: 256
|
| 7 |
+
edge_dim: 0
|
| 8 |
+
num_r_gaussian: 64
|
| 9 |
+
act_fn: relu
|
| 10 |
+
norm: False
|
| 11 |
+
cutoff: 10.0
|
| 12 |
+
knn: 48 # kNN Graph
|
| 13 |
+
|
| 14 |
+
train:
|
| 15 |
+
seed: 2021
|
| 16 |
+
batch_size: 4
|
| 17 |
+
num_workers: 4
|
| 18 |
+
max_epochs: 200
|
| 19 |
+
report_iter: 200
|
| 20 |
+
val_freq: 1
|
| 21 |
+
pos_noise_std: 0.1
|
| 22 |
+
max_grad_norm: 10.
|
| 23 |
+
optimizer:
|
| 24 |
+
type: adam
|
| 25 |
+
lr: 1.e-4
|
| 26 |
+
weight_decay: 0
|
| 27 |
+
beta1: 0.99
|
| 28 |
+
beta2: 0.999
|
| 29 |
+
scheduler:
|
| 30 |
+
type: plateau
|
| 31 |
+
factor: 0.6
|
| 32 |
+
patience: 10
|
| 33 |
+
min_lr: 1.e-5
|
| 34 |
+
|
| 35 |
+
dataset:
|
| 36 |
+
name: pdbbind
|
| 37 |
+
heavy_only: True
|
| 38 |
+
path: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/pdbbind_v2020/pocket_10_general
|
| 39 |
+
split: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/pdbbind_v2020/pocket_10_general/split.pt
|
configs/prop/pdbbind_general_egnn_enc_final_h.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
hidden_channels: 256
|
| 3 |
+
enc_ligand_dim: 0
|
| 4 |
+
enc_node_dim: 128
|
| 5 |
+
enc_graph_dim: 0
|
| 6 |
+
enc_feature_type: final_h
|
| 7 |
+
encoder:
|
| 8 |
+
name: egnn_enc
|
| 9 |
+
num_layers: 6
|
| 10 |
+
hidden_dim: 256
|
| 11 |
+
edge_dim: 0
|
| 12 |
+
num_r_gaussian: 64
|
| 13 |
+
act_fn: relu
|
| 14 |
+
norm: False
|
| 15 |
+
cutoff: 10.0
|
| 16 |
+
knn: 48 # kNN Graph
|
| 17 |
+
|
| 18 |
+
train:
|
| 19 |
+
seed: 2021
|
| 20 |
+
batch_size: 4
|
| 21 |
+
num_workers: 4
|
| 22 |
+
max_epochs: 200
|
| 23 |
+
report_iter: 200
|
| 24 |
+
val_freq: 1
|
| 25 |
+
pos_noise_std: 0.1
|
| 26 |
+
max_grad_norm: 10.
|
| 27 |
+
optimizer:
|
| 28 |
+
type: adam
|
| 29 |
+
lr: 1.e-4
|
| 30 |
+
weight_decay: 0
|
| 31 |
+
beta1: 0.99
|
| 32 |
+
beta2: 0.999
|
| 33 |
+
scheduler:
|
| 34 |
+
type: plateau
|
| 35 |
+
factor: 0.6
|
| 36 |
+
patience: 10
|
| 37 |
+
min_lr: 1.e-5
|
| 38 |
+
|
| 39 |
+
dataset:
|
| 40 |
+
name: pdbbind
|
| 41 |
+
heavy_only: True
|
| 42 |
+
path: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/pdbbind_v2020/pocket_10_general
|
| 43 |
+
split: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/pdbbind_v2020/pocket_10_general/split.pt
|
| 44 |
+
emb_path: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/pdbbind_v2020/pdbbind_v2020_all_match_emb.pt
|
configs/sampling.yml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model:
|
| 2 |
+
checkpoint: /public/home/liuyx19/onescience-examples/models/biosciences/TargetDiff/weight/pretrained_diffusion.pt
|
| 3 |
+
|
| 4 |
+
sample:
|
| 5 |
+
seed: 2021
|
| 6 |
+
num_samples: 100
|
| 7 |
+
num_steps: 1000
|
| 8 |
+
pos_only: False
|
| 9 |
+
center_pos_mode: protein
|
| 10 |
+
sample_num_atoms: prior
|
configs/training.yml
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
data:
|
| 2 |
+
name: pl
|
| 3 |
+
path: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/crossdocked_v1.1_rmsd1.0_pocket10
|
| 4 |
+
split: ${ONESCIENCE_DATASETS_DIR}/targetdiff/data/crossdocked_pocket10_pose_split.pt
|
| 5 |
+
transform:
|
| 6 |
+
ligand_atom_mode: add_aromatic
|
| 7 |
+
random_rot: False
|
| 8 |
+
|
| 9 |
+
model:
|
| 10 |
+
model_mean_type: C0 # ['noise', 'C0']
|
| 11 |
+
beta_schedule: sigmoid
|
| 12 |
+
beta_start: 1.e-7
|
| 13 |
+
beta_end: 2.e-3
|
| 14 |
+
v_beta_schedule: cosine
|
| 15 |
+
v_beta_s: 0.01
|
| 16 |
+
num_diffusion_timesteps: 1000
|
| 17 |
+
loss_v_weight: 100.
|
| 18 |
+
sample_time_method: symmetric # ['importance', 'symmetric']
|
| 19 |
+
|
| 20 |
+
time_emb_dim: 0
|
| 21 |
+
time_emb_mode: simple
|
| 22 |
+
center_pos_mode: protein
|
| 23 |
+
|
| 24 |
+
node_indicator: True
|
| 25 |
+
model_type: uni_o2
|
| 26 |
+
num_blocks: 1
|
| 27 |
+
num_layers: 9
|
| 28 |
+
hidden_dim: 128
|
| 29 |
+
n_heads: 16
|
| 30 |
+
edge_feat_dim: 4 # edge type feat
|
| 31 |
+
num_r_gaussian: 20
|
| 32 |
+
knn: 32 # !
|
| 33 |
+
num_node_types: 8
|
| 34 |
+
act_fn: relu
|
| 35 |
+
norm: True
|
| 36 |
+
cutoff_mode: knn # [radius, none]
|
| 37 |
+
ew_net_type: global # [r, m, none]
|
| 38 |
+
num_x2h: 1
|
| 39 |
+
num_h2x: 1
|
| 40 |
+
r_max: 10.
|
| 41 |
+
x2h_out_fc: False
|
| 42 |
+
sync_twoup: False
|
| 43 |
+
|
| 44 |
+
train:
|
| 45 |
+
seed: 2021
|
| 46 |
+
batch_size: 4
|
| 47 |
+
num_workers: 4
|
| 48 |
+
n_acc_batch: 1
|
| 49 |
+
max_iters: 10000000
|
| 50 |
+
val_freq: 100
|
| 51 |
+
pos_noise_std: 0.1
|
| 52 |
+
max_grad_norm: 8.0
|
| 53 |
+
bond_loss_weight: 1.0
|
| 54 |
+
optimizer:
|
| 55 |
+
type: adam
|
| 56 |
+
lr: 5.e-4
|
| 57 |
+
weight_decay: 0
|
| 58 |
+
beta1: 0.95
|
| 59 |
+
beta2: 0.999
|
| 60 |
+
scheduler:
|
| 61 |
+
type: plateau
|
| 62 |
+
factor: 0.6
|
| 63 |
+
patience: 10
|
| 64 |
+
min_lr: 1.e-6
|
examples/3ug2_ligand.sdf
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
3ug2_ligand
|
| 2 |
+
|
| 3 |
+
Created by X-TOOL on Fri Sep 26 17:34:55 2014
|
| 4 |
+
56 59 0 0 0 0 0 0 0 0999 V2000
|
| 5 |
+
3.4040 46.1170 14.5070 C 0 0 0 3 0 4
|
| 6 |
+
4.3860 44.9380 14.3750 C 0 0 0 3 0 4
|
| 7 |
+
3.6900 43.6880 14.5540 O 0 0 0 1 0 2
|
| 8 |
+
3.0800 43.4440 15.8490 C 0 0 0 3 0 4
|
| 9 |
+
2.7940 44.7300 16.6440 C 0 0 0 3 0 4
|
| 10 |
+
2.5950 45.8870 15.7230 N 0 3 0 2 0 4
|
| 11 |
+
1.5560 46.8550 16.0410 C 0 0 0 3 0 4
|
| 12 |
+
1.9860 47.4510 17.3890 C 0 0 0 3 0 4
|
| 13 |
+
0.7930 47.7880 18.2910 C 0 0 0 3 0 4
|
| 14 |
+
-0.0010 48.8270 17.6860 O 0 0 0 1 0 2
|
| 15 |
+
-0.5460 49.8840 18.3700 C 0 0 0 1 0 3
|
| 16 |
+
-1.1750 50.8290 17.5810 C 0 0 0 1 0 3
|
| 17 |
+
-1.7490 51.9430 18.1780 C 0 0 0 2 0 3
|
| 18 |
+
-1.2170 50.6790 16.2200 O 0 0 0 1 0 2
|
| 19 |
+
-2.3380 51.2600 15.5080 C 0 0 0 4 0 4
|
| 20 |
+
-0.5160 50.0310 19.7780 C 0 0 0 2 0 3
|
| 21 |
+
-1.1300 51.1410 20.3560 C 0 0 0 1 0 3
|
| 22 |
+
-1.7470 52.0880 19.5480 C 0 0 0 1 0 3
|
| 23 |
+
-2.3440 53.1660 20.0860 N 0 0 0 1 0 2
|
| 24 |
+
-2.3130 53.3880 21.3930 C 0 0 0 2 0 3
|
| 25 |
+
-1.7230 52.5050 22.1960 N 0 0 0 1 0 2
|
| 26 |
+
-1.1370 51.3830 21.7250 C 0 0 0 1 0 3
|
| 27 |
+
-0.5500 50.5160 22.5680 N 0 0 0 2 0 3
|
| 28 |
+
-0.7890 50.3540 23.9050 C 0 0 0 1 0 3
|
| 29 |
+
-2.0680 50.5030 24.4450 C 0 0 0 2 0 3
|
| 30 |
+
-2.2860 50.3360 25.8070 C 0 0 0 1 0 3
|
| 31 |
+
-3.8770 50.5280 26.6050 Cl 0 0 0 1 0 1
|
| 32 |
+
-1.2480 49.9960 26.6110 C 0 0 0 1 0 3
|
| 33 |
+
-1.5490 49.8410 27.9240 F 0 0 0 1 0 1
|
| 34 |
+
0.0430 49.8020 26.0870 C 0 0 0 2 0 3
|
| 35 |
+
0.2670 49.9660 24.7340 C 0 0 0 2 0 3
|
| 36 |
+
2.7500 46.1619 13.6237 H 0 0 0 1 0 1
|
| 37 |
+
3.9608 47.0611 14.5993 H 0 0 0 1 0 1
|
| 38 |
+
5.1698 45.0304 15.1412 H 0 0 0 1 0 1
|
| 39 |
+
4.8457 44.9581 13.3759 H 0 0 0 1 0 1
|
| 40 |
+
3.7592 42.8126 16.4406 H 0 0 0 1 0 1
|
| 41 |
+
2.1295 42.9131 15.6919 H 0 0 0 1 0 1
|
| 42 |
+
3.6448 44.9401 17.3088 H 0 0 0 1 0 1
|
| 43 |
+
1.8845 44.5874 17.2461 H 0 0 0 1 0 1
|
| 44 |
+
1.9433 45.3395 15.1831 H 0 0 0 1 0 1
|
| 45 |
+
0.5774 46.3604 16.1290 H 0 0 0 1 0 1
|
| 46 |
+
1.5036 47.6369 15.2691 H 0 0 0 1 0 1
|
| 47 |
+
2.5570 48.3722 17.2010 H 0 0 0 1 0 1
|
| 48 |
+
2.6259 46.7225 17.9085 H 0 0 0 1 0 1
|
| 49 |
+
1.1608 48.1326 19.2687 H 0 0 0 1 0 1
|
| 50 |
+
0.1740 46.8891 18.4282 H 0 0 0 1 0 1
|
| 51 |
+
-2.2041 52.7077 17.5590 H 0 0 0 1 0 1
|
| 52 |
+
-2.2330 51.0584 14.4317 H 0 0 0 1 0 1
|
| 53 |
+
-2.3565 52.3467 15.6774 H 0 0 0 1 0 1
|
| 54 |
+
-3.2752 50.8157 15.8745 H 0 0 0 1 0 1
|
| 55 |
+
-0.0226 49.2909 20.3976 H 0 0 0 1 0 1
|
| 56 |
+
-2.7652 54.2829 21.8051 H 0 0 0 1 0 1
|
| 57 |
+
0.1659 49.8995 22.1578 H 0 0 0 1 0 1
|
| 58 |
+
-2.8992 50.7515 23.7951 H 0 0 0 1 0 1
|
| 59 |
+
0.8599 49.5245 26.7434 H 0 0 0 1 0 1
|
| 60 |
+
1.2536 49.7952 24.3187 H 0 0 0 1 0 1
|
| 61 |
+
1 2 1 0 0 1
|
| 62 |
+
1 6 1 0 0 1
|
| 63 |
+
2 3 1 0 0 1
|
| 64 |
+
3 4 1 0 0 1
|
| 65 |
+
4 5 1 0 0 1
|
| 66 |
+
5 6 1 0 0 1
|
| 67 |
+
6 7 1 0 0 2
|
| 68 |
+
7 8 1 0 0 2
|
| 69 |
+
8 9 1 0 0 2
|
| 70 |
+
9 10 1 0 0 2
|
| 71 |
+
10 11 1 0 0 2
|
| 72 |
+
11 12 4 0 0 1
|
| 73 |
+
11 16 4 0 0 1
|
| 74 |
+
12 13 4 0 0 1
|
| 75 |
+
12 14 1 0 0 2
|
| 76 |
+
13 18 4 0 0 1
|
| 77 |
+
14 15 1 0 0 2
|
| 78 |
+
16 17 4 0 0 1
|
| 79 |
+
17 18 4 0 0 1
|
| 80 |
+
17 22 4 0 0 1
|
| 81 |
+
18 19 4 0 0 1
|
| 82 |
+
19 20 4 0 0 1
|
| 83 |
+
20 21 4 0 0 1
|
| 84 |
+
21 22 4 0 0 1
|
| 85 |
+
22 23 1 0 0 2
|
| 86 |
+
23 24 1 0 0 2
|
| 87 |
+
24 25 4 0 0 1
|
| 88 |
+
24 31 4 0 0 1
|
| 89 |
+
25 26 4 0 0 1
|
| 90 |
+
26 27 1 0 0 2
|
| 91 |
+
26 28 4 0 0 1
|
| 92 |
+
28 29 1 0 0 2
|
| 93 |
+
28 30 4 0 0 1
|
| 94 |
+
30 31 4 0 0 1
|
| 95 |
+
1 32 1 0 0 2
|
| 96 |
+
1 33 1 0 0 2
|
| 97 |
+
2 34 1 0 0 2
|
| 98 |
+
2 35 1 0 0 2
|
| 99 |
+
4 36 1 0 0 2
|
| 100 |
+
4 37 1 0 0 2
|
| 101 |
+
5 38 1 0 0 2
|
| 102 |
+
5 39 1 0 0 2
|
| 103 |
+
6 40 1 0 0 2
|
| 104 |
+
7 41 1 0 0 2
|
| 105 |
+
7 42 1 0 0 2
|
| 106 |
+
8 43 1 0 0 2
|
| 107 |
+
8 44 1 0 0 2
|
| 108 |
+
9 45 1 0 0 2
|
| 109 |
+
9 46 1 0 0 2
|
| 110 |
+
13 47 1 0 0 2
|
| 111 |
+
15 48 1 0 0 2
|
| 112 |
+
15 49 1 0 0 2
|
| 113 |
+
15 50 1 0 0 2
|
| 114 |
+
16 51 1 0 0 2
|
| 115 |
+
20 52 1 0 0 2
|
| 116 |
+
23 53 1 0 0 2
|
| 117 |
+
25 54 1 0 0 2
|
| 118 |
+
30 55 1 0 0 2
|
| 119 |
+
31 56 1 0 0 2
|
| 120 |
+
M END
|
| 121 |
+
> <MOLECULAR_FORMULA>
|
| 122 |
+
C22H25N4O3FCl
|
| 123 |
+
|
| 124 |
+
> <MOLECULAR_WEIGHT>
|
| 125 |
+
447.7
|
| 126 |
+
|
| 127 |
+
> <NUM_HB_ATOMS>
|
| 128 |
+
7
|
| 129 |
+
|
| 130 |
+
> <NUM_ROTOR>
|
| 131 |
+
6
|
| 132 |
+
|
| 133 |
+
> <XLOGP2>
|
| 134 |
+
3.77
|
| 135 |
+
|
| 136 |
+
$$$$
|
examples/3ug2_protein.pdb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
models/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TargetDiff model components."""
|
| 2 |
+
|
models/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (212 Bytes). View file
|
|
|
models/__pycache__/common.cpython-311.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
models/__pycache__/egnn.cpython-311.pyc
ADDED
|
Binary file (8.28 kB). View file
|
|
|
models/__pycache__/molopt_score_model.cpython-311.pyc
ADDED
|
Binary file (36.4 kB). View file
|
|
|
models/__pycache__/uni_transformer.cpython-311.pyc
ADDED
|
Binary file (18.4 kB). View file
|
|
|
models/common.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch_geometric.nn import knn_graph
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class GaussianSmearing(nn.Module):
|
| 8 |
+
def __init__(self, start=0.0, stop=5.0, num_gaussians=50, fixed_offset=True):
|
| 9 |
+
super(GaussianSmearing, self).__init__()
|
| 10 |
+
self.start = start
|
| 11 |
+
self.stop = stop
|
| 12 |
+
self.num_gaussians = num_gaussians
|
| 13 |
+
if fixed_offset:
|
| 14 |
+
# customized offset
|
| 15 |
+
offset = torch.tensor([0, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.5, 4, 4.5, 5, 5.5, 6, 7, 8, 9, 10])
|
| 16 |
+
else:
|
| 17 |
+
offset = torch.linspace(start, stop, num_gaussians)
|
| 18 |
+
self.coeff = -0.5 / (offset[1] - offset[0]).item() ** 2
|
| 19 |
+
self.register_buffer('offset', offset)
|
| 20 |
+
|
| 21 |
+
def __repr__(self):
|
| 22 |
+
return f'GaussianSmearing(start={self.start}, stop={self.stop}, num_gaussians={self.num_gaussians})'
|
| 23 |
+
|
| 24 |
+
def forward(self, dist):
|
| 25 |
+
dist = dist.view(-1, 1) - self.offset.view(1, -1)
|
| 26 |
+
return torch.exp(self.coeff * torch.pow(dist, 2))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class AngleExpansion(nn.Module):
|
| 30 |
+
def __init__(self, start=1.0, stop=5.0, half_expansion=10):
|
| 31 |
+
super(AngleExpansion, self).__init__()
|
| 32 |
+
l_mul = 1. / torch.linspace(stop, start, half_expansion)
|
| 33 |
+
r_mul = torch.linspace(start, stop, half_expansion)
|
| 34 |
+
coeff = torch.cat([l_mul, r_mul], dim=-1)
|
| 35 |
+
self.register_buffer('coeff', coeff)
|
| 36 |
+
|
| 37 |
+
def forward(self, angle):
|
| 38 |
+
return torch.cos(angle.view(-1, 1) * self.coeff.view(1, -1))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class Swish(nn.Module):
|
| 42 |
+
def __init__(self):
|
| 43 |
+
super(Swish, self).__init__()
|
| 44 |
+
self.beta = nn.Parameter(torch.tensor(1.0))
|
| 45 |
+
|
| 46 |
+
def forward(self, x):
|
| 47 |
+
return x * torch.sigmoid(self.beta * x)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
NONLINEARITIES = {
|
| 51 |
+
"tanh": nn.Tanh(),
|
| 52 |
+
"relu": nn.ReLU(),
|
| 53 |
+
"softplus": nn.Softplus(),
|
| 54 |
+
"elu": nn.ELU(),
|
| 55 |
+
"swish": Swish(),
|
| 56 |
+
'silu': nn.SiLU()
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class MLP(nn.Module):
|
| 61 |
+
"""MLP with the same hidden dim across all layers."""
|
| 62 |
+
|
| 63 |
+
def __init__(self, in_dim, out_dim, hidden_dim, num_layer=2, norm=True, act_fn='relu', act_last=False):
|
| 64 |
+
super().__init__()
|
| 65 |
+
layers = []
|
| 66 |
+
for layer_idx in range(num_layer):
|
| 67 |
+
if layer_idx == 0:
|
| 68 |
+
layers.append(nn.Linear(in_dim, hidden_dim))
|
| 69 |
+
elif layer_idx == num_layer - 1:
|
| 70 |
+
layers.append(nn.Linear(hidden_dim, out_dim))
|
| 71 |
+
else:
|
| 72 |
+
layers.append(nn.Linear(hidden_dim, hidden_dim))
|
| 73 |
+
if layer_idx < num_layer - 1 or act_last:
|
| 74 |
+
if norm:
|
| 75 |
+
layers.append(nn.LayerNorm(hidden_dim))
|
| 76 |
+
layers.append(NONLINEARITIES[act_fn])
|
| 77 |
+
self.net = nn.Sequential(*layers)
|
| 78 |
+
|
| 79 |
+
def forward(self, x):
|
| 80 |
+
return self.net(x)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def outer_product(*vectors):
|
| 84 |
+
for index, vector in enumerate(vectors):
|
| 85 |
+
if index == 0:
|
| 86 |
+
out = vector.unsqueeze(-1)
|
| 87 |
+
else:
|
| 88 |
+
out = out * vector.unsqueeze(1)
|
| 89 |
+
out = out.view(out.shape[0], -1).unsqueeze(-1)
|
| 90 |
+
return out.squeeze()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def get_h_dist(dist_metric, hi, hj):
|
| 94 |
+
if dist_metric == 'euclidean':
|
| 95 |
+
h_dist = torch.sum((hi - hj) ** 2, -1, keepdim=True)
|
| 96 |
+
return h_dist
|
| 97 |
+
elif dist_metric == 'cos_sim':
|
| 98 |
+
hi_norm = torch.norm(hi, p=2, dim=-1, keepdim=True)
|
| 99 |
+
hj_norm = torch.norm(hj, p=2, dim=-1, keepdim=True)
|
| 100 |
+
h_dist = torch.sum(hi * hj, -1, keepdim=True) / (hi_norm * hj_norm)
|
| 101 |
+
return h_dist, hj_norm
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_r_feat(r, r_exp_func, node_type=None, edge_index=None, mode='basic'):
|
| 105 |
+
if mode == 'origin':
|
| 106 |
+
r_feat = r
|
| 107 |
+
elif mode == 'basic':
|
| 108 |
+
r_feat = r_exp_func(r)
|
| 109 |
+
elif mode == 'sparse':
|
| 110 |
+
src, dst = edge_index
|
| 111 |
+
nt_src = node_type[src] # [n_edges, 8]
|
| 112 |
+
nt_dst = node_type[dst]
|
| 113 |
+
r_exp = r_exp_func(r)
|
| 114 |
+
r_feat = outer_product(nt_src, nt_dst, r_exp)
|
| 115 |
+
else:
|
| 116 |
+
raise ValueError(mode)
|
| 117 |
+
return r_feat
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def compose_context(h_protein, h_ligand, pos_protein, pos_ligand, batch_protein, batch_ligand):
|
| 121 |
+
# previous version has problems when ligand atom types are fixed
|
| 122 |
+
# (due to sorting randomly in case of same element)
|
| 123 |
+
|
| 124 |
+
batch_ctx = torch.cat([batch_protein, batch_ligand], dim=0)
|
| 125 |
+
# sort_idx = batch_ctx.argsort()
|
| 126 |
+
sort_idx = torch.sort(batch_ctx, stable=True).indices
|
| 127 |
+
|
| 128 |
+
mask_ligand = torch.cat([
|
| 129 |
+
torch.zeros([batch_protein.size(0)], device=batch_protein.device).bool(),
|
| 130 |
+
torch.ones([batch_ligand.size(0)], device=batch_ligand.device).bool(),
|
| 131 |
+
], dim=0)[sort_idx]
|
| 132 |
+
|
| 133 |
+
batch_ctx = batch_ctx[sort_idx]
|
| 134 |
+
h_ctx = torch.cat([h_protein, h_ligand], dim=0)[sort_idx] # (N_protein+N_ligand, H)
|
| 135 |
+
pos_ctx = torch.cat([pos_protein, pos_ligand], dim=0)[sort_idx] # (N_protein+N_ligand, 3)
|
| 136 |
+
|
| 137 |
+
return h_ctx, pos_ctx, batch_ctx, mask_ligand
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def compose_context_prop(h_protein, h_ligand, pos_protein, pos_ligand, batch_protein, batch_ligand):
|
| 141 |
+
batch_ctx = torch.cat([batch_protein, batch_ligand], dim=0)
|
| 142 |
+
sort_idx = batch_ctx.argsort()
|
| 143 |
+
|
| 144 |
+
mask_protein = torch.cat([
|
| 145 |
+
torch.ones([batch_protein.size(0)], device=batch_protein.device).bool(),
|
| 146 |
+
torch.zeros([batch_ligand.size(0)], device=batch_ligand.device).bool(),
|
| 147 |
+
], dim=0)[sort_idx]
|
| 148 |
+
|
| 149 |
+
batch_ctx = batch_ctx[sort_idx]
|
| 150 |
+
h_ctx = torch.cat([h_protein, h_ligand], dim=0)[sort_idx] # (N_protein+N_ligand, H)
|
| 151 |
+
pos_ctx = torch.cat([pos_protein, pos_ligand], dim=0)[sort_idx] # (N_protein+N_ligand, 3)
|
| 152 |
+
|
| 153 |
+
return h_ctx, pos_ctx, batch_ctx
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class ShiftedSoftplus(nn.Module):
|
| 157 |
+
def __init__(self):
|
| 158 |
+
super().__init__()
|
| 159 |
+
self.shift = torch.log(torch.tensor(2.0)).item()
|
| 160 |
+
|
| 161 |
+
def forward(self, x):
|
| 162 |
+
return F.softplus(x) - self.shift
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def hybrid_edge_connection(ligand_pos, protein_pos, k, ligand_index, protein_index):
|
| 166 |
+
# fully-connected for ligand atoms
|
| 167 |
+
dst = torch.repeat_interleave(ligand_index, len(ligand_index))
|
| 168 |
+
src = ligand_index.repeat(len(ligand_index))
|
| 169 |
+
mask = dst != src
|
| 170 |
+
dst, src = dst[mask], src[mask]
|
| 171 |
+
ll_edge_index = torch.stack([src, dst])
|
| 172 |
+
|
| 173 |
+
# knn for ligand-protein edges
|
| 174 |
+
ligand_protein_pos_dist = torch.unsqueeze(ligand_pos, 1) - torch.unsqueeze(protein_pos, 0)
|
| 175 |
+
ligand_protein_pos_dist = torch.norm(ligand_protein_pos_dist, p=2, dim=-1)
|
| 176 |
+
knn_p_idx = torch.topk(ligand_protein_pos_dist, k=k, largest=False, dim=1).indices
|
| 177 |
+
knn_p_idx = protein_index[knn_p_idx]
|
| 178 |
+
knn_l_idx = torch.unsqueeze(ligand_index, 1)
|
| 179 |
+
knn_l_idx = knn_l_idx.repeat(1, k)
|
| 180 |
+
pl_edge_index = torch.stack([knn_p_idx, knn_l_idx], dim=0)
|
| 181 |
+
pl_edge_index = pl_edge_index.view(2, -1)
|
| 182 |
+
return ll_edge_index, pl_edge_index
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def batch_hybrid_edge_connection(x, k, mask_ligand, batch, add_p_index=False):
|
| 186 |
+
batch_size = batch.max().item() + 1
|
| 187 |
+
batch_ll_edge_index, batch_pl_edge_index, batch_p_edge_index = [], [], []
|
| 188 |
+
with torch.no_grad():
|
| 189 |
+
for i in range(batch_size):
|
| 190 |
+
ligand_index = ((batch == i) & (mask_ligand == 1)).nonzero()[:, 0]
|
| 191 |
+
protein_index = ((batch == i) & (mask_ligand == 0)).nonzero()[:, 0]
|
| 192 |
+
ligand_pos, protein_pos = x[ligand_index], x[protein_index]
|
| 193 |
+
ll_edge_index, pl_edge_index = hybrid_edge_connection(
|
| 194 |
+
ligand_pos, protein_pos, k, ligand_index, protein_index)
|
| 195 |
+
batch_ll_edge_index.append(ll_edge_index)
|
| 196 |
+
batch_pl_edge_index.append(pl_edge_index)
|
| 197 |
+
if add_p_index:
|
| 198 |
+
all_pos = torch.cat([protein_pos, ligand_pos], 0)
|
| 199 |
+
p_edge_index = knn_graph(all_pos, k=k, flow='source_to_target')
|
| 200 |
+
p_edge_index = p_edge_index[:, p_edge_index[1] < len(protein_pos)]
|
| 201 |
+
p_src, p_dst = p_edge_index
|
| 202 |
+
all_index = torch.cat([protein_index, ligand_index], 0)
|
| 203 |
+
p_edge_index = torch.stack([all_index[p_src], all_index[p_dst]], 0)
|
| 204 |
+
batch_p_edge_index.append(p_edge_index)
|
| 205 |
+
|
| 206 |
+
if add_p_index:
|
| 207 |
+
edge_index = [torch.cat([ll, pl, p], -1) for ll, pl, p in zip(
|
| 208 |
+
batch_ll_edge_index, batch_pl_edge_index, batch_p_edge_index)]
|
| 209 |
+
else:
|
| 210 |
+
edge_index = [torch.cat([ll, pl], -1) for ll, pl in zip(batch_ll_edge_index, batch_pl_edge_index)]
|
| 211 |
+
edge_index = torch.cat(edge_index, -1)
|
| 212 |
+
return edge_index
|
models/egnn.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch_scatter import scatter_sum
|
| 5 |
+
from torch_geometric.nn import radius_graph, knn_graph
|
| 6 |
+
from .common import GaussianSmearing, MLP, batch_hybrid_edge_connection, NONLINEARITIES
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class EnBaseLayer(nn.Module):
|
| 10 |
+
def __init__(self, hidden_dim, edge_feat_dim, num_r_gaussian, update_x=True, act_fn='silu', norm=False):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.r_min = 0.
|
| 13 |
+
self.r_max = 10.
|
| 14 |
+
self.hidden_dim = hidden_dim
|
| 15 |
+
self.num_r_gaussian = num_r_gaussian
|
| 16 |
+
self.edge_feat_dim = edge_feat_dim
|
| 17 |
+
self.update_x = update_x
|
| 18 |
+
self.act_fn = act_fn
|
| 19 |
+
self.norm = norm
|
| 20 |
+
if num_r_gaussian > 1:
|
| 21 |
+
self.distance_expansion = GaussianSmearing(self.r_min, self.r_max, num_gaussians=num_r_gaussian)
|
| 22 |
+
self.edge_mlp = MLP(2 * hidden_dim + edge_feat_dim + num_r_gaussian, hidden_dim, hidden_dim,
|
| 23 |
+
num_layer=2, norm=norm, act_fn=act_fn, act_last=True)
|
| 24 |
+
self.edge_inf = nn.Sequential(nn.Linear(hidden_dim, 1), nn.Sigmoid())
|
| 25 |
+
if self.update_x:
|
| 26 |
+
# self.x_mlp = MLP(hidden_dim, 1, hidden_dim, num_layer=2, norm=norm, act_fn=act_fn)
|
| 27 |
+
x_mlp = [nn.Linear(hidden_dim, hidden_dim), NONLINEARITIES[act_fn]]
|
| 28 |
+
layer = nn.Linear(hidden_dim, 1, bias=False)
|
| 29 |
+
torch.nn.init.xavier_uniform_(layer.weight, gain=0.001)
|
| 30 |
+
x_mlp.append(layer)
|
| 31 |
+
x_mlp.append(nn.Tanh())
|
| 32 |
+
self.x_mlp = nn.Sequential(*x_mlp)
|
| 33 |
+
|
| 34 |
+
self.node_mlp = MLP(2 * hidden_dim, hidden_dim, hidden_dim, num_layer=2, norm=norm, act_fn=act_fn)
|
| 35 |
+
|
| 36 |
+
def forward(self, h, x, edge_index, mask_ligand, edge_attr=None):
|
| 37 |
+
src, dst = edge_index
|
| 38 |
+
hi, hj = h[dst], h[src]
|
| 39 |
+
# \phi_e in Eq(3)
|
| 40 |
+
rel_x = x[dst] - x[src]
|
| 41 |
+
d_sq = torch.sum(rel_x ** 2, -1, keepdim=True)
|
| 42 |
+
if self.num_r_gaussian > 1:
|
| 43 |
+
d_feat = self.distance_expansion(torch.sqrt(d_sq + 1e-8))
|
| 44 |
+
else:
|
| 45 |
+
d_feat = d_sq
|
| 46 |
+
if edge_attr is not None:
|
| 47 |
+
edge_feat = torch.cat([d_feat, edge_attr], -1)
|
| 48 |
+
else:
|
| 49 |
+
edge_feat = d_sq
|
| 50 |
+
|
| 51 |
+
mij = self.edge_mlp(torch.cat([hi, hj, edge_feat], -1))
|
| 52 |
+
eij = self.edge_inf(mij)
|
| 53 |
+
mi = scatter_sum(mij * eij, dst, dim=0, dim_size=h.shape[0])
|
| 54 |
+
|
| 55 |
+
# h update in Eq(6)
|
| 56 |
+
h = h + self.node_mlp(torch.cat([mi, h], -1))
|
| 57 |
+
if self.update_x:
|
| 58 |
+
# x update in Eq(4)
|
| 59 |
+
xi, xj = x[dst], x[src]
|
| 60 |
+
# (xi - xj) / (\|xi - xj\| + C) to make it more stable
|
| 61 |
+
delta_x = scatter_sum((xi - xj) / (torch.sqrt(d_sq + 1e-8) + 1) * self.x_mlp(mij), dst, dim=0)
|
| 62 |
+
x = x + delta_x * mask_ligand[:, None] # only ligand positions will be updated
|
| 63 |
+
|
| 64 |
+
return h, x
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class EGNN(nn.Module):
|
| 68 |
+
def __init__(self, num_layers, hidden_dim, edge_feat_dim, num_r_gaussian, k=32, cutoff=10.0, cutoff_mode='knn',
|
| 69 |
+
update_x=True, act_fn='silu', norm=False):
|
| 70 |
+
super().__init__()
|
| 71 |
+
# Build the network
|
| 72 |
+
self.num_layers = num_layers
|
| 73 |
+
self.hidden_dim = hidden_dim
|
| 74 |
+
self.edge_feat_dim = edge_feat_dim
|
| 75 |
+
self.num_r_gaussian = num_r_gaussian
|
| 76 |
+
self.update_x = update_x
|
| 77 |
+
self.act_fn = act_fn
|
| 78 |
+
self.norm = norm
|
| 79 |
+
self.k = k
|
| 80 |
+
self.cutoff = cutoff
|
| 81 |
+
self.cutoff_mode = cutoff_mode
|
| 82 |
+
self.distance_expansion = GaussianSmearing(stop=cutoff, num_gaussians=num_r_gaussian)
|
| 83 |
+
self.net = self._build_network()
|
| 84 |
+
|
| 85 |
+
def _build_network(self):
|
| 86 |
+
# Equivariant layers
|
| 87 |
+
layers = []
|
| 88 |
+
for l_idx in range(self.num_layers):
|
| 89 |
+
layer = EnBaseLayer(self.hidden_dim, self.edge_feat_dim, self.num_r_gaussian,
|
| 90 |
+
update_x=self.update_x, act_fn=self.act_fn, norm=self.norm)
|
| 91 |
+
layers.append(layer)
|
| 92 |
+
return nn.ModuleList(layers)
|
| 93 |
+
|
| 94 |
+
# todo: refactor
|
| 95 |
+
def _connect_edge(self, x, mask_ligand, batch):
|
| 96 |
+
# if self.cutoff_mode == 'radius':
|
| 97 |
+
# edge_index = radius_graph(x, r=self.r, batch=batch, flow='source_to_target')
|
| 98 |
+
if self.cutoff_mode == 'knn':
|
| 99 |
+
edge_index = knn_graph(x, k=self.k, batch=batch, flow='source_to_target')
|
| 100 |
+
elif self.cutoff_mode == 'hybrid':
|
| 101 |
+
edge_index = batch_hybrid_edge_connection(
|
| 102 |
+
x, k=self.k, mask_ligand=mask_ligand, batch=batch, add_p_index=True)
|
| 103 |
+
else:
|
| 104 |
+
raise ValueError(f'Not supported cutoff mode: {self.cutoff_mode}')
|
| 105 |
+
return edge_index
|
| 106 |
+
|
| 107 |
+
# todo: refactor
|
| 108 |
+
@staticmethod
|
| 109 |
+
def _build_edge_type(edge_index, mask_ligand):
|
| 110 |
+
src, dst = edge_index
|
| 111 |
+
edge_type = torch.zeros(len(src)).to(edge_index)
|
| 112 |
+
n_src = mask_ligand[src] == 1
|
| 113 |
+
n_dst = mask_ligand[dst] == 1
|
| 114 |
+
edge_type[n_src & n_dst] = 0
|
| 115 |
+
edge_type[n_src & ~n_dst] = 1
|
| 116 |
+
edge_type[~n_src & n_dst] = 2
|
| 117 |
+
edge_type[~n_src & ~n_dst] = 3
|
| 118 |
+
edge_type = F.one_hot(edge_type, num_classes=4)
|
| 119 |
+
return edge_type
|
| 120 |
+
|
| 121 |
+
def forward(self, h, x, mask_ligand, batch, return_all=False):
|
| 122 |
+
all_x = [x]
|
| 123 |
+
all_h = [h]
|
| 124 |
+
for l_idx, layer in enumerate(self.net):
|
| 125 |
+
edge_index = self._connect_edge(x, mask_ligand, batch)
|
| 126 |
+
edge_type = self._build_edge_type(edge_index, mask_ligand)
|
| 127 |
+
h, x = layer(h, x, edge_index, mask_ligand, edge_attr=edge_type)
|
| 128 |
+
all_x.append(x)
|
| 129 |
+
all_h.append(h)
|
| 130 |
+
outputs = {'x': x, 'h': h}
|
| 131 |
+
if return_all:
|
| 132 |
+
outputs.update({'all_x': all_x, 'all_h': all_h})
|
| 133 |
+
return outputs
|
models/molopt_score_model.py
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch_scatter import scatter_sum, scatter_mean
|
| 6 |
+
from tqdm.auto import tqdm
|
| 7 |
+
|
| 8 |
+
from .common import compose_context, ShiftedSoftplus
|
| 9 |
+
from .egnn import EGNN
|
| 10 |
+
from .uni_transformer import UniTransformerO2TwoUpdateGeneral
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_refine_net(refine_net_type, config):
|
| 14 |
+
if refine_net_type == 'uni_o2':
|
| 15 |
+
refine_net = UniTransformerO2TwoUpdateGeneral(
|
| 16 |
+
num_blocks=config.num_blocks,
|
| 17 |
+
num_layers=config.num_layers,
|
| 18 |
+
hidden_dim=config.hidden_dim,
|
| 19 |
+
n_heads=config.n_heads,
|
| 20 |
+
k=config.knn,
|
| 21 |
+
edge_feat_dim=config.edge_feat_dim,
|
| 22 |
+
num_r_gaussian=config.num_r_gaussian,
|
| 23 |
+
num_node_types=config.num_node_types,
|
| 24 |
+
act_fn=config.act_fn,
|
| 25 |
+
norm=config.norm,
|
| 26 |
+
cutoff_mode=config.cutoff_mode,
|
| 27 |
+
ew_net_type=config.ew_net_type,
|
| 28 |
+
num_x2h=config.num_x2h,
|
| 29 |
+
num_h2x=config.num_h2x,
|
| 30 |
+
r_max=config.r_max,
|
| 31 |
+
x2h_out_fc=config.x2h_out_fc,
|
| 32 |
+
sync_twoup=config.sync_twoup
|
| 33 |
+
)
|
| 34 |
+
elif refine_net_type == 'egnn':
|
| 35 |
+
refine_net = EGNN(
|
| 36 |
+
num_layers=config.num_layers,
|
| 37 |
+
hidden_dim=config.hidden_dim,
|
| 38 |
+
edge_feat_dim=config.edge_feat_dim,
|
| 39 |
+
num_r_gaussian=1,
|
| 40 |
+
k=config.knn,
|
| 41 |
+
cutoff_mode=config.cutoff_mode
|
| 42 |
+
)
|
| 43 |
+
else:
|
| 44 |
+
raise ValueError(refine_net_type)
|
| 45 |
+
return refine_net
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def get_beta_schedule(beta_schedule, *, beta_start, beta_end, num_diffusion_timesteps):
|
| 49 |
+
def sigmoid(x):
|
| 50 |
+
return 1 / (np.exp(-x) + 1)
|
| 51 |
+
|
| 52 |
+
if beta_schedule == "quad":
|
| 53 |
+
betas = (
|
| 54 |
+
np.linspace(
|
| 55 |
+
beta_start ** 0.5,
|
| 56 |
+
beta_end ** 0.5,
|
| 57 |
+
num_diffusion_timesteps,
|
| 58 |
+
dtype=np.float64,
|
| 59 |
+
)
|
| 60 |
+
** 2
|
| 61 |
+
)
|
| 62 |
+
elif beta_schedule == "linear":
|
| 63 |
+
betas = np.linspace(
|
| 64 |
+
beta_start, beta_end, num_diffusion_timesteps, dtype=np.float64
|
| 65 |
+
)
|
| 66 |
+
elif beta_schedule == "const":
|
| 67 |
+
betas = beta_end * np.ones(num_diffusion_timesteps, dtype=np.float64)
|
| 68 |
+
elif beta_schedule == "jsd": # 1/T, 1/(T-1), 1/(T-2), ..., 1
|
| 69 |
+
betas = 1.0 / np.linspace(
|
| 70 |
+
num_diffusion_timesteps, 1, num_diffusion_timesteps, dtype=np.float64
|
| 71 |
+
)
|
| 72 |
+
elif beta_schedule == "sigmoid":
|
| 73 |
+
betas = np.linspace(-6, 6, num_diffusion_timesteps)
|
| 74 |
+
betas = sigmoid(betas) * (beta_end - beta_start) + beta_start
|
| 75 |
+
else:
|
| 76 |
+
raise NotImplementedError(beta_schedule)
|
| 77 |
+
assert betas.shape == (num_diffusion_timesteps,)
|
| 78 |
+
return betas
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def cosine_beta_schedule(timesteps, s=0.008):
|
| 82 |
+
"""
|
| 83 |
+
cosine schedule
|
| 84 |
+
as proposed in https://openreview.net/forum?id=-NEXDKk8gZ
|
| 85 |
+
"""
|
| 86 |
+
steps = timesteps + 1
|
| 87 |
+
x = np.linspace(0, steps, steps)
|
| 88 |
+
alphas_cumprod = np.cos(((x / steps) + s) / (1 + s) * np.pi * 0.5) ** 2
|
| 89 |
+
alphas_cumprod = alphas_cumprod / alphas_cumprod[0]
|
| 90 |
+
alphas = (alphas_cumprod[1:] / alphas_cumprod[:-1])
|
| 91 |
+
|
| 92 |
+
alphas = np.clip(alphas, a_min=0.001, a_max=1.)
|
| 93 |
+
|
| 94 |
+
# Use sqrt of this, so the alpha in our paper is the alpha_sqrt from the
|
| 95 |
+
# Gaussian diffusion in Ho et al.
|
| 96 |
+
alphas = np.sqrt(alphas)
|
| 97 |
+
return alphas
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def get_distance(pos, edge_index):
|
| 101 |
+
return (pos[edge_index[0]] - pos[edge_index[1]]).norm(dim=-1)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def to_torch_const(x):
|
| 105 |
+
x = torch.from_numpy(x).float()
|
| 106 |
+
x = nn.Parameter(x, requires_grad=False)
|
| 107 |
+
return x
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def center_pos(protein_pos, ligand_pos, batch_protein, batch_ligand, mode='protein'):
|
| 111 |
+
if mode == 'none':
|
| 112 |
+
offset = 0.
|
| 113 |
+
pass
|
| 114 |
+
elif mode == 'protein':
|
| 115 |
+
offset = scatter_mean(protein_pos, batch_protein, dim=0)
|
| 116 |
+
protein_pos = protein_pos - offset[batch_protein]
|
| 117 |
+
ligand_pos = ligand_pos - offset[batch_ligand]
|
| 118 |
+
else:
|
| 119 |
+
raise NotImplementedError
|
| 120 |
+
return protein_pos, ligand_pos, offset
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# %% categorical diffusion related
|
| 124 |
+
def index_to_log_onehot(x, num_classes):
|
| 125 |
+
assert x.max().item() < num_classes, f'Error: {x.max().item()} >= {num_classes}'
|
| 126 |
+
x_onehot = F.one_hot(x, num_classes)
|
| 127 |
+
# permute_order = (0, -1) + tuple(range(1, len(x.size())))
|
| 128 |
+
# x_onehot = x_onehot.permute(permute_order)
|
| 129 |
+
log_x = torch.log(x_onehot.float().clamp(min=1e-30))
|
| 130 |
+
return log_x
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def log_onehot_to_index(log_x):
|
| 134 |
+
return log_x.argmax(1)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def categorical_kl(log_prob1, log_prob2):
|
| 138 |
+
kl = (log_prob1.exp() * (log_prob1 - log_prob2)).sum(dim=1)
|
| 139 |
+
return kl
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def log_categorical(log_x_start, log_prob):
|
| 143 |
+
return (log_x_start.exp() * log_prob).sum(dim=1)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def normal_kl(mean1, logvar1, mean2, logvar2):
|
| 147 |
+
"""
|
| 148 |
+
KL divergence between normal distributions parameterized by mean and log-variance.
|
| 149 |
+
"""
|
| 150 |
+
kl = 0.5 * (-1.0 + logvar2 - logvar1 + torch.exp(logvar1 - logvar2) + (mean1 - mean2) ** 2 * torch.exp(-logvar2))
|
| 151 |
+
return kl.sum(-1)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def log_normal(values, means, log_scales):
|
| 155 |
+
var = torch.exp(log_scales * 2)
|
| 156 |
+
log_prob = -((values - means) ** 2) / (2 * var) - log_scales - np.log(np.sqrt(2 * np.pi))
|
| 157 |
+
return log_prob.sum(-1)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def log_sample_categorical(logits):
|
| 161 |
+
uniform = torch.rand_like(logits)
|
| 162 |
+
gumbel_noise = -torch.log(-torch.log(uniform + 1e-30) + 1e-30)
|
| 163 |
+
sample_index = (gumbel_noise + logits).argmax(dim=-1)
|
| 164 |
+
# sample_onehot = F.one_hot(sample, self.num_classes)
|
| 165 |
+
# log_sample = index_to_log_onehot(sample, self.num_classes)
|
| 166 |
+
return sample_index
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def log_1_min_a(a):
|
| 170 |
+
return np.log(1 - np.exp(a) + 1e-40)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def log_add_exp(a, b):
|
| 174 |
+
maximum = torch.max(a, b)
|
| 175 |
+
return maximum + torch.log(torch.exp(a - maximum) + torch.exp(b - maximum))
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# %%
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# Time embedding
|
| 182 |
+
class SinusoidalPosEmb(nn.Module):
|
| 183 |
+
def __init__(self, dim):
|
| 184 |
+
super().__init__()
|
| 185 |
+
self.dim = dim
|
| 186 |
+
|
| 187 |
+
def forward(self, x):
|
| 188 |
+
device = x.device
|
| 189 |
+
half_dim = self.dim // 2
|
| 190 |
+
emb = np.log(10000) / (half_dim - 1)
|
| 191 |
+
emb = torch.exp(torch.arange(half_dim, device=device) * -emb)
|
| 192 |
+
emb = x[:, None] * emb[None, :]
|
| 193 |
+
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
|
| 194 |
+
return emb
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# Model
|
| 198 |
+
class ScorePosNet3D(nn.Module):
|
| 199 |
+
|
| 200 |
+
def __init__(self, config, protein_atom_feature_dim, ligand_atom_feature_dim):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.config = config
|
| 203 |
+
|
| 204 |
+
# variance schedule
|
| 205 |
+
self.model_mean_type = config.model_mean_type # ['noise', 'C0']
|
| 206 |
+
self.loss_v_weight = config.loss_v_weight
|
| 207 |
+
# self.v_mode = config.v_mode
|
| 208 |
+
# assert self.v_mode == 'categorical'
|
| 209 |
+
# self.v_net_type = getattr(config, 'v_net_type', 'mlp')
|
| 210 |
+
# self.bond_loss = getattr(config, 'bond_loss', False)
|
| 211 |
+
# self.bond_net_type = getattr(config, 'bond_net_type', 'pre_att')
|
| 212 |
+
# self.loss_bond_weight = getattr(config, 'loss_bond_weight', 0.)
|
| 213 |
+
# self.loss_non_bond_weight = getattr(config, 'loss_non_bond_weight', 0.)
|
| 214 |
+
|
| 215 |
+
self.sample_time_method = config.sample_time_method # ['importance', 'symmetric']
|
| 216 |
+
# self.loss_pos_type = config.loss_pos_type # ['mse', 'kl']
|
| 217 |
+
# print(f'Loss pos mode {self.loss_pos_type} applied!')
|
| 218 |
+
# print(f'Loss bond net type: {self.bond_net_type} '
|
| 219 |
+
# f'bond weight: {self.loss_bond_weight} non bond weight: {self.loss_non_bond_weight}')
|
| 220 |
+
|
| 221 |
+
if config.beta_schedule == 'cosine':
|
| 222 |
+
alphas = cosine_beta_schedule(config.num_diffusion_timesteps, config.pos_beta_s) ** 2
|
| 223 |
+
# print('cosine pos alpha schedule applied!')
|
| 224 |
+
betas = 1. - alphas
|
| 225 |
+
else:
|
| 226 |
+
betas = get_beta_schedule(
|
| 227 |
+
beta_schedule=config.beta_schedule,
|
| 228 |
+
beta_start=config.beta_start,
|
| 229 |
+
beta_end=config.beta_end,
|
| 230 |
+
num_diffusion_timesteps=config.num_diffusion_timesteps,
|
| 231 |
+
)
|
| 232 |
+
alphas = 1. - betas
|
| 233 |
+
alphas_cumprod = np.cumprod(alphas, axis=0)
|
| 234 |
+
alphas_cumprod_prev = np.append(1., alphas_cumprod[:-1])
|
| 235 |
+
|
| 236 |
+
self.betas = to_torch_const(betas)
|
| 237 |
+
self.num_timesteps = self.betas.size(0)
|
| 238 |
+
self.alphas_cumprod = to_torch_const(alphas_cumprod)
|
| 239 |
+
self.alphas_cumprod_prev = to_torch_const(alphas_cumprod_prev)
|
| 240 |
+
|
| 241 |
+
# calculations for diffusion q(x_t | x_{t-1}) and others
|
| 242 |
+
self.sqrt_alphas_cumprod = to_torch_const(np.sqrt(alphas_cumprod))
|
| 243 |
+
self.sqrt_one_minus_alphas_cumprod = to_torch_const(np.sqrt(1. - alphas_cumprod))
|
| 244 |
+
self.sqrt_recip_alphas_cumprod = to_torch_const(np.sqrt(1. / alphas_cumprod))
|
| 245 |
+
self.sqrt_recipm1_alphas_cumprod = to_torch_const(np.sqrt(1. / alphas_cumprod - 1))
|
| 246 |
+
|
| 247 |
+
# calculations for posterior q(x_{t-1} | x_t, x_0)
|
| 248 |
+
posterior_variance = betas * (1. - alphas_cumprod_prev) / (1. - alphas_cumprod)
|
| 249 |
+
self.posterior_mean_c0_coef = to_torch_const(betas * np.sqrt(alphas_cumprod_prev) / (1. - alphas_cumprod))
|
| 250 |
+
self.posterior_mean_ct_coef = to_torch_const(
|
| 251 |
+
(1. - alphas_cumprod_prev) * np.sqrt(alphas) / (1. - alphas_cumprod))
|
| 252 |
+
# log calculation clipped because the posterior variance is 0 at the beginning of the diffusion chain
|
| 253 |
+
self.posterior_var = to_torch_const(posterior_variance)
|
| 254 |
+
self.posterior_logvar = to_torch_const(np.log(np.append(self.posterior_var[1], self.posterior_var[1:])))
|
| 255 |
+
|
| 256 |
+
# atom type diffusion schedule in log space
|
| 257 |
+
if config.v_beta_schedule == 'cosine':
|
| 258 |
+
alphas_v = cosine_beta_schedule(self.num_timesteps, config.v_beta_s)
|
| 259 |
+
# print('cosine v alpha schedule applied!')
|
| 260 |
+
else:
|
| 261 |
+
raise NotImplementedError
|
| 262 |
+
log_alphas_v = np.log(alphas_v)
|
| 263 |
+
log_alphas_cumprod_v = np.cumsum(log_alphas_v)
|
| 264 |
+
self.log_alphas_v = to_torch_const(log_alphas_v)
|
| 265 |
+
self.log_one_minus_alphas_v = to_torch_const(log_1_min_a(log_alphas_v))
|
| 266 |
+
self.log_alphas_cumprod_v = to_torch_const(log_alphas_cumprod_v)
|
| 267 |
+
self.log_one_minus_alphas_cumprod_v = to_torch_const(log_1_min_a(log_alphas_cumprod_v))
|
| 268 |
+
|
| 269 |
+
self.register_buffer('Lt_history', torch.zeros(self.num_timesteps))
|
| 270 |
+
self.register_buffer('Lt_count', torch.zeros(self.num_timesteps))
|
| 271 |
+
|
| 272 |
+
# model definition
|
| 273 |
+
self.hidden_dim = config.hidden_dim
|
| 274 |
+
self.num_classes = ligand_atom_feature_dim
|
| 275 |
+
if self.config.node_indicator:
|
| 276 |
+
emb_dim = self.hidden_dim - 1
|
| 277 |
+
else:
|
| 278 |
+
emb_dim = self.hidden_dim
|
| 279 |
+
|
| 280 |
+
# atom embedding
|
| 281 |
+
self.protein_atom_emb = nn.Linear(protein_atom_feature_dim, emb_dim)
|
| 282 |
+
|
| 283 |
+
# center pos
|
| 284 |
+
self.center_pos_mode = config.center_pos_mode # ['none', 'protein']
|
| 285 |
+
|
| 286 |
+
# time embedding
|
| 287 |
+
self.time_emb_dim = config.time_emb_dim
|
| 288 |
+
self.time_emb_mode = config.time_emb_mode # ['simple', 'sin']
|
| 289 |
+
if self.time_emb_dim > 0:
|
| 290 |
+
if self.time_emb_mode == 'simple':
|
| 291 |
+
self.ligand_atom_emb = nn.Linear(ligand_atom_feature_dim + 1, emb_dim)
|
| 292 |
+
elif self.time_emb_mode == 'sin':
|
| 293 |
+
self.time_emb = nn.Sequential(
|
| 294 |
+
SinusoidalPosEmb(self.time_emb_dim),
|
| 295 |
+
nn.Linear(self.time_emb_dim, self.time_emb_dim * 4),
|
| 296 |
+
nn.GELU(),
|
| 297 |
+
nn.Linear(self.time_emb_dim * 4, self.time_emb_dim)
|
| 298 |
+
)
|
| 299 |
+
self.ligand_atom_emb = nn.Linear(ligand_atom_feature_dim + self.time_emb_dim, emb_dim)
|
| 300 |
+
else:
|
| 301 |
+
raise NotImplementedError
|
| 302 |
+
else:
|
| 303 |
+
self.ligand_atom_emb = nn.Linear(ligand_atom_feature_dim, emb_dim)
|
| 304 |
+
|
| 305 |
+
self.refine_net_type = config.model_type
|
| 306 |
+
self.refine_net = get_refine_net(self.refine_net_type, config)
|
| 307 |
+
self.v_inference = nn.Sequential(
|
| 308 |
+
nn.Linear(self.hidden_dim, self.hidden_dim),
|
| 309 |
+
ShiftedSoftplus(),
|
| 310 |
+
nn.Linear(self.hidden_dim, ligand_atom_feature_dim),
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
def forward(self, protein_pos, protein_v, batch_protein, init_ligand_pos, init_ligand_v, batch_ligand,
|
| 314 |
+
time_step=None, return_all=False, fix_x=False):
|
| 315 |
+
|
| 316 |
+
batch_size = batch_protein.max().item() + 1
|
| 317 |
+
init_ligand_v = F.one_hot(init_ligand_v, self.num_classes).float()
|
| 318 |
+
# time embedding
|
| 319 |
+
if self.time_emb_dim > 0:
|
| 320 |
+
if self.time_emb_mode == 'simple':
|
| 321 |
+
input_ligand_feat = torch.cat([
|
| 322 |
+
init_ligand_v,
|
| 323 |
+
(time_step / self.num_timesteps)[batch_ligand].unsqueeze(-1)
|
| 324 |
+
], -1)
|
| 325 |
+
elif self.time_emb_mode == 'sin':
|
| 326 |
+
time_feat = self.time_emb(time_step)
|
| 327 |
+
input_ligand_feat = torch.cat([init_ligand_v, time_feat], -1)
|
| 328 |
+
else:
|
| 329 |
+
raise NotImplementedError
|
| 330 |
+
else:
|
| 331 |
+
input_ligand_feat = init_ligand_v
|
| 332 |
+
|
| 333 |
+
h_protein = self.protein_atom_emb(protein_v)
|
| 334 |
+
init_ligand_h = self.ligand_atom_emb(input_ligand_feat)
|
| 335 |
+
|
| 336 |
+
if self.config.node_indicator:
|
| 337 |
+
h_protein = torch.cat([h_protein, torch.zeros(len(h_protein), 1).to(h_protein)], -1)
|
| 338 |
+
init_ligand_h = torch.cat([init_ligand_h, torch.ones(len(init_ligand_h), 1).to(h_protein)], -1)
|
| 339 |
+
|
| 340 |
+
h_all, pos_all, batch_all, mask_ligand = compose_context(
|
| 341 |
+
h_protein=h_protein,
|
| 342 |
+
h_ligand=init_ligand_h,
|
| 343 |
+
pos_protein=protein_pos,
|
| 344 |
+
pos_ligand=init_ligand_pos,
|
| 345 |
+
batch_protein=batch_protein,
|
| 346 |
+
batch_ligand=batch_ligand,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
outputs = self.refine_net(h_all, pos_all, mask_ligand, batch_all, return_all=return_all, fix_x=fix_x)
|
| 350 |
+
final_pos, final_h = outputs['x'], outputs['h']
|
| 351 |
+
final_ligand_pos, final_ligand_h = final_pos[mask_ligand], final_h[mask_ligand]
|
| 352 |
+
final_ligand_v = self.v_inference(final_ligand_h)
|
| 353 |
+
|
| 354 |
+
preds = {
|
| 355 |
+
'pred_ligand_pos': final_ligand_pos,
|
| 356 |
+
'pred_ligand_v': final_ligand_v,
|
| 357 |
+
'final_h': final_h,
|
| 358 |
+
'final_ligand_h': final_ligand_h
|
| 359 |
+
}
|
| 360 |
+
if return_all:
|
| 361 |
+
final_all_pos, final_all_h = outputs['all_x'], outputs['all_h']
|
| 362 |
+
final_all_ligand_pos = [pos[mask_ligand] for pos in final_all_pos]
|
| 363 |
+
final_all_ligand_v = [self.v_inference(h[mask_ligand]) for h in final_all_h]
|
| 364 |
+
preds.update({
|
| 365 |
+
'layer_pred_ligand_pos': final_all_ligand_pos,
|
| 366 |
+
'layer_pred_ligand_v': final_all_ligand_v
|
| 367 |
+
})
|
| 368 |
+
return preds
|
| 369 |
+
|
| 370 |
+
# atom type diffusion process
|
| 371 |
+
def q_v_pred_one_timestep(self, log_vt_1, t, batch):
|
| 372 |
+
# q(vt | vt-1)
|
| 373 |
+
log_alpha_t = extract(self.log_alphas_v, t, batch)
|
| 374 |
+
log_1_min_alpha_t = extract(self.log_one_minus_alphas_v, t, batch)
|
| 375 |
+
|
| 376 |
+
# alpha_t * vt + (1 - alpha_t) 1 / K
|
| 377 |
+
log_probs = log_add_exp(
|
| 378 |
+
log_vt_1 + log_alpha_t,
|
| 379 |
+
log_1_min_alpha_t - np.log(self.num_classes)
|
| 380 |
+
)
|
| 381 |
+
return log_probs
|
| 382 |
+
|
| 383 |
+
def q_v_pred(self, log_v0, t, batch):
|
| 384 |
+
# compute q(vt | v0)
|
| 385 |
+
log_cumprod_alpha_t = extract(self.log_alphas_cumprod_v, t, batch)
|
| 386 |
+
log_1_min_cumprod_alpha = extract(self.log_one_minus_alphas_cumprod_v, t, batch)
|
| 387 |
+
|
| 388 |
+
log_probs = log_add_exp(
|
| 389 |
+
log_v0 + log_cumprod_alpha_t,
|
| 390 |
+
log_1_min_cumprod_alpha - np.log(self.num_classes)
|
| 391 |
+
)
|
| 392 |
+
return log_probs
|
| 393 |
+
|
| 394 |
+
def q_v_sample(self, log_v0, t, batch):
|
| 395 |
+
log_qvt_v0 = self.q_v_pred(log_v0, t, batch)
|
| 396 |
+
sample_index = log_sample_categorical(log_qvt_v0)
|
| 397 |
+
log_sample = index_to_log_onehot(sample_index, self.num_classes)
|
| 398 |
+
return sample_index, log_sample
|
| 399 |
+
|
| 400 |
+
# atom type generative process
|
| 401 |
+
def q_v_posterior(self, log_v0, log_vt, t, batch):
|
| 402 |
+
# q(vt-1 | vt, v0) = q(vt | vt-1, x0) * q(vt-1 | x0) / q(vt | x0)
|
| 403 |
+
t_minus_1 = t - 1
|
| 404 |
+
# Remove negative values, will not be used anyway for final decoder
|
| 405 |
+
t_minus_1 = torch.where(t_minus_1 < 0, torch.zeros_like(t_minus_1), t_minus_1)
|
| 406 |
+
log_qvt1_v0 = self.q_v_pred(log_v0, t_minus_1, batch)
|
| 407 |
+
unnormed_logprobs = log_qvt1_v0 + self.q_v_pred_one_timestep(log_vt, t, batch)
|
| 408 |
+
log_vt1_given_vt_v0 = unnormed_logprobs - torch.logsumexp(unnormed_logprobs, dim=-1, keepdim=True)
|
| 409 |
+
return log_vt1_given_vt_v0
|
| 410 |
+
|
| 411 |
+
def kl_v_prior(self, log_x_start, batch):
|
| 412 |
+
num_graphs = batch.max().item() + 1
|
| 413 |
+
log_qxT_prob = self.q_v_pred(log_x_start, t=[self.num_timesteps - 1] * num_graphs, batch=batch)
|
| 414 |
+
log_half_prob = -torch.log(self.num_classes * torch.ones_like(log_qxT_prob))
|
| 415 |
+
kl_prior = categorical_kl(log_qxT_prob, log_half_prob)
|
| 416 |
+
kl_prior = scatter_mean(kl_prior, batch, dim=0)
|
| 417 |
+
return kl_prior
|
| 418 |
+
|
| 419 |
+
def _predict_x0_from_eps(self, xt, eps, t, batch):
|
| 420 |
+
pos0_from_e = extract(self.sqrt_recip_alphas_cumprod, t, batch) * xt - \
|
| 421 |
+
extract(self.sqrt_recipm1_alphas_cumprod, t, batch) * eps
|
| 422 |
+
return pos0_from_e
|
| 423 |
+
|
| 424 |
+
def q_pos_posterior(self, x0, xt, t, batch):
|
| 425 |
+
# Compute the mean and variance of the diffusion posterior q(x_{t-1} | x_t, x_0)
|
| 426 |
+
pos_model_mean = extract(self.posterior_mean_c0_coef, t, batch) * x0 + \
|
| 427 |
+
extract(self.posterior_mean_ct_coef, t, batch) * xt
|
| 428 |
+
return pos_model_mean
|
| 429 |
+
|
| 430 |
+
def kl_pos_prior(self, pos0, batch):
|
| 431 |
+
num_graphs = batch.max().item() + 1
|
| 432 |
+
a_pos = extract(self.alphas_cumprod, [self.num_timesteps - 1] * num_graphs, batch) # (num_ligand_atoms, 1)
|
| 433 |
+
pos_model_mean = a_pos.sqrt() * pos0
|
| 434 |
+
pos_log_variance = torch.log((1.0 - a_pos).sqrt())
|
| 435 |
+
kl_prior = normal_kl(torch.zeros_like(pos_model_mean), torch.zeros_like(pos_log_variance),
|
| 436 |
+
pos_model_mean, pos_log_variance)
|
| 437 |
+
kl_prior = scatter_mean(kl_prior, batch, dim=0)
|
| 438 |
+
return kl_prior
|
| 439 |
+
|
| 440 |
+
def sample_time(self, num_graphs, device, method):
|
| 441 |
+
if method == 'importance':
|
| 442 |
+
if not (self.Lt_count > 10).all():
|
| 443 |
+
return self.sample_time(num_graphs, device, method='symmetric')
|
| 444 |
+
|
| 445 |
+
Lt_sqrt = torch.sqrt(self.Lt_history + 1e-10) + 0.0001
|
| 446 |
+
Lt_sqrt[0] = Lt_sqrt[1] # Overwrite decoder term with L1.
|
| 447 |
+
pt_all = Lt_sqrt / Lt_sqrt.sum()
|
| 448 |
+
|
| 449 |
+
time_step = torch.multinomial(pt_all, num_samples=num_graphs, replacement=True)
|
| 450 |
+
pt = pt_all.gather(dim=0, index=time_step)
|
| 451 |
+
return time_step, pt
|
| 452 |
+
|
| 453 |
+
elif method == 'symmetric':
|
| 454 |
+
time_step = torch.randint(
|
| 455 |
+
0, self.num_timesteps, size=(num_graphs // 2 + 1,), device=device)
|
| 456 |
+
time_step = torch.cat(
|
| 457 |
+
[time_step, self.num_timesteps - time_step - 1], dim=0)[:num_graphs]
|
| 458 |
+
pt = torch.ones_like(time_step).float() / self.num_timesteps
|
| 459 |
+
return time_step, pt
|
| 460 |
+
|
| 461 |
+
else:
|
| 462 |
+
raise ValueError
|
| 463 |
+
|
| 464 |
+
def compute_pos_Lt(self, pos_model_mean, x0, xt, t, batch):
|
| 465 |
+
# fixed pos variance
|
| 466 |
+
pos_log_variance = extract(self.posterior_logvar, t, batch)
|
| 467 |
+
pos_true_mean = self.q_pos_posterior(x0=x0, xt=xt, t=t, batch=batch)
|
| 468 |
+
kl_pos = normal_kl(pos_true_mean, pos_log_variance, pos_model_mean, pos_log_variance)
|
| 469 |
+
kl_pos = kl_pos / np.log(2.)
|
| 470 |
+
|
| 471 |
+
decoder_nll_pos = -log_normal(x0, means=pos_model_mean, log_scales=0.5 * pos_log_variance)
|
| 472 |
+
assert kl_pos.shape == decoder_nll_pos.shape
|
| 473 |
+
mask = (t == 0).float()[batch]
|
| 474 |
+
loss_pos = scatter_mean(mask * decoder_nll_pos + (1. - mask) * kl_pos, batch, dim=0)
|
| 475 |
+
return loss_pos
|
| 476 |
+
|
| 477 |
+
def compute_v_Lt(self, log_v_model_prob, log_v0, log_v_true_prob, t, batch):
|
| 478 |
+
kl_v = categorical_kl(log_v_true_prob, log_v_model_prob) # [num_atoms, ]
|
| 479 |
+
decoder_nll_v = -log_categorical(log_v0, log_v_model_prob) # L0
|
| 480 |
+
assert kl_v.shape == decoder_nll_v.shape
|
| 481 |
+
mask = (t == 0).float()[batch]
|
| 482 |
+
loss_v = scatter_mean(mask * decoder_nll_v + (1. - mask) * kl_v, batch, dim=0)
|
| 483 |
+
return loss_v
|
| 484 |
+
|
| 485 |
+
def get_diffusion_loss(
|
| 486 |
+
self, protein_pos, protein_v, batch_protein, ligand_pos, ligand_v, batch_ligand, time_step=None
|
| 487 |
+
):
|
| 488 |
+
num_graphs = batch_protein.max().item() + 1
|
| 489 |
+
protein_pos, ligand_pos, _ = center_pos(
|
| 490 |
+
protein_pos, ligand_pos, batch_protein, batch_ligand, mode=self.center_pos_mode)
|
| 491 |
+
|
| 492 |
+
# 1. sample noise levels
|
| 493 |
+
if time_step is None:
|
| 494 |
+
time_step, pt = self.sample_time(num_graphs, protein_pos.device, self.sample_time_method)
|
| 495 |
+
else:
|
| 496 |
+
pt = torch.ones_like(time_step).float() / self.num_timesteps
|
| 497 |
+
a = self.alphas_cumprod.index_select(0, time_step) # (num_graphs, )
|
| 498 |
+
|
| 499 |
+
# 2. perturb pos and v
|
| 500 |
+
a_pos = a[batch_ligand].unsqueeze(-1) # (num_ligand_atoms, 1)
|
| 501 |
+
pos_noise = torch.zeros_like(ligand_pos)
|
| 502 |
+
pos_noise.normal_()
|
| 503 |
+
# Xt = a.sqrt() * X0 + (1-a).sqrt() * eps
|
| 504 |
+
ligand_pos_perturbed = a_pos.sqrt() * ligand_pos + (1.0 - a_pos).sqrt() * pos_noise # pos_noise * std
|
| 505 |
+
# Vt = a * V0 + (1-a) / K
|
| 506 |
+
log_ligand_v0 = index_to_log_onehot(ligand_v, self.num_classes)
|
| 507 |
+
ligand_v_perturbed, log_ligand_vt = self.q_v_sample(log_ligand_v0, time_step, batch_ligand)
|
| 508 |
+
|
| 509 |
+
# 3. forward-pass NN, feed perturbed pos and v, output noise
|
| 510 |
+
preds = self(
|
| 511 |
+
protein_pos=protein_pos,
|
| 512 |
+
protein_v=protein_v,
|
| 513 |
+
batch_protein=batch_protein,
|
| 514 |
+
|
| 515 |
+
init_ligand_pos=ligand_pos_perturbed,
|
| 516 |
+
init_ligand_v=ligand_v_perturbed,
|
| 517 |
+
batch_ligand=batch_ligand,
|
| 518 |
+
time_step=time_step
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
pred_ligand_pos, pred_ligand_v = preds['pred_ligand_pos'], preds['pred_ligand_v']
|
| 522 |
+
pred_pos_noise = pred_ligand_pos - ligand_pos_perturbed
|
| 523 |
+
# atom position
|
| 524 |
+
if self.model_mean_type == 'noise':
|
| 525 |
+
pos0_from_e = self._predict_x0_from_eps(
|
| 526 |
+
xt=ligand_pos_perturbed, eps=pred_pos_noise, t=time_step, batch=batch_ligand)
|
| 527 |
+
pos_model_mean = self.q_pos_posterior(
|
| 528 |
+
x0=pos0_from_e, xt=ligand_pos_perturbed, t=time_step, batch=batch_ligand)
|
| 529 |
+
elif self.model_mean_type == 'C0':
|
| 530 |
+
pos_model_mean = self.q_pos_posterior(
|
| 531 |
+
x0=pred_ligand_pos, xt=ligand_pos_perturbed, t=time_step, batch=batch_ligand)
|
| 532 |
+
else:
|
| 533 |
+
raise ValueError
|
| 534 |
+
|
| 535 |
+
# atom pos loss
|
| 536 |
+
if self.model_mean_type == 'C0':
|
| 537 |
+
target, pred = ligand_pos, pred_ligand_pos
|
| 538 |
+
elif self.model_mean_type == 'noise':
|
| 539 |
+
target, pred = pos_noise, pred_pos_noise
|
| 540 |
+
else:
|
| 541 |
+
raise ValueError
|
| 542 |
+
loss_pos = scatter_mean(((pred - target) ** 2).sum(-1), batch_ligand, dim=0)
|
| 543 |
+
loss_pos = torch.mean(loss_pos)
|
| 544 |
+
|
| 545 |
+
# atom type loss
|
| 546 |
+
log_ligand_v_recon = F.log_softmax(pred_ligand_v, dim=-1)
|
| 547 |
+
log_v_model_prob = self.q_v_posterior(log_ligand_v_recon, log_ligand_vt, time_step, batch_ligand)
|
| 548 |
+
log_v_true_prob = self.q_v_posterior(log_ligand_v0, log_ligand_vt, time_step, batch_ligand)
|
| 549 |
+
kl_v = self.compute_v_Lt(log_v_model_prob=log_v_model_prob, log_v0=log_ligand_v0,
|
| 550 |
+
log_v_true_prob=log_v_true_prob, t=time_step, batch=batch_ligand)
|
| 551 |
+
loss_v = torch.mean(kl_v)
|
| 552 |
+
loss = loss_pos + loss_v * self.loss_v_weight
|
| 553 |
+
|
| 554 |
+
return {
|
| 555 |
+
'loss_pos': loss_pos,
|
| 556 |
+
'loss_v': loss_v,
|
| 557 |
+
'loss': loss,
|
| 558 |
+
'x0': ligand_pos,
|
| 559 |
+
'pred_ligand_pos': pred_ligand_pos,
|
| 560 |
+
'pred_ligand_v': pred_ligand_v,
|
| 561 |
+
'pred_pos_noise': pred_pos_noise,
|
| 562 |
+
'ligand_v_recon': F.softmax(pred_ligand_v, dim=-1)
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
@torch.no_grad()
|
| 566 |
+
def likelihood_estimation(
|
| 567 |
+
self, protein_pos, protein_v, batch_protein, ligand_pos, ligand_v, batch_ligand, time_step
|
| 568 |
+
):
|
| 569 |
+
protein_pos, ligand_pos, _ = center_pos(
|
| 570 |
+
protein_pos, ligand_pos, batch_protein, batch_ligand, mode='protein')
|
| 571 |
+
assert (time_step == self.num_timesteps).all() or (time_step < self.num_timesteps).all()
|
| 572 |
+
if (time_step == self.num_timesteps).all():
|
| 573 |
+
kl_pos_prior = self.kl_pos_prior(ligand_pos, batch_ligand)
|
| 574 |
+
log_ligand_v0 = index_to_log_onehot(batch_ligand, self.num_classes)
|
| 575 |
+
kl_v_prior = self.kl_v_prior(log_ligand_v0, batch_ligand)
|
| 576 |
+
return kl_pos_prior, kl_v_prior
|
| 577 |
+
|
| 578 |
+
# perturb pos and v
|
| 579 |
+
a = self.alphas_cumprod.index_select(0, time_step) # (num_graphs, )
|
| 580 |
+
a_pos = a[batch_ligand].unsqueeze(-1) # (num_ligand_atoms, 1)
|
| 581 |
+
pos_noise = torch.zeros_like(ligand_pos)
|
| 582 |
+
pos_noise.normal_()
|
| 583 |
+
# Xt = a.sqrt() * X0 + (1-a).sqrt() * eps
|
| 584 |
+
ligand_pos_perturbed = a_pos.sqrt() * ligand_pos + (1.0 - a_pos).sqrt() * pos_noise # pos_noise * std
|
| 585 |
+
# Vt = a * V0 + (1-a) / K
|
| 586 |
+
log_ligand_v0 = index_to_log_onehot(ligand_v, self.num_classes)
|
| 587 |
+
ligand_v_perturbed, log_ligand_vt = self.q_v_sample(log_ligand_v0, time_step, batch_ligand)
|
| 588 |
+
|
| 589 |
+
preds = self(
|
| 590 |
+
protein_pos=protein_pos,
|
| 591 |
+
protein_v=protein_v,
|
| 592 |
+
batch_protein=batch_protein,
|
| 593 |
+
|
| 594 |
+
init_ligand_pos=ligand_pos_perturbed,
|
| 595 |
+
init_ligand_v=ligand_v_perturbed,
|
| 596 |
+
batch_ligand=batch_ligand,
|
| 597 |
+
time_step=time_step
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
pred_ligand_pos, pred_ligand_v = preds['pred_ligand_pos'], preds['pred_ligand_v']
|
| 601 |
+
if self.model_mean_type == 'C0':
|
| 602 |
+
pos_model_mean = self.q_pos_posterior(
|
| 603 |
+
x0=pred_ligand_pos, xt=ligand_pos_perturbed, t=time_step, batch=batch_ligand)
|
| 604 |
+
else:
|
| 605 |
+
raise ValueError
|
| 606 |
+
|
| 607 |
+
# atom type
|
| 608 |
+
log_ligand_v_recon = F.log_softmax(pred_ligand_v, dim=-1)
|
| 609 |
+
log_v_model_prob = self.q_v_posterior(log_ligand_v_recon, log_ligand_vt, time_step, batch_ligand)
|
| 610 |
+
log_v_true_prob = self.q_v_posterior(log_ligand_v0, log_ligand_vt, time_step, batch_ligand)
|
| 611 |
+
|
| 612 |
+
# t = [T-1, ... , 0]
|
| 613 |
+
kl_pos = self.compute_pos_Lt(pos_model_mean=pos_model_mean, x0=ligand_pos,
|
| 614 |
+
xt=ligand_pos_perturbed, t=time_step, batch=batch_ligand)
|
| 615 |
+
kl_v = self.compute_v_Lt(log_v_model_prob=log_v_model_prob, log_v0=log_ligand_v0,
|
| 616 |
+
log_v_true_prob=log_v_true_prob, t=time_step, batch=batch_ligand)
|
| 617 |
+
return kl_pos, kl_v
|
| 618 |
+
|
| 619 |
+
@torch.no_grad()
|
| 620 |
+
def fetch_embedding(self, protein_pos, protein_v, batch_protein, ligand_pos, ligand_v, batch_ligand):
|
| 621 |
+
preds = self(
|
| 622 |
+
protein_pos=protein_pos,
|
| 623 |
+
protein_v=protein_v,
|
| 624 |
+
batch_protein=batch_protein,
|
| 625 |
+
|
| 626 |
+
init_ligand_pos=ligand_pos,
|
| 627 |
+
init_ligand_v=ligand_v,
|
| 628 |
+
batch_ligand=batch_ligand,
|
| 629 |
+
fix_x=True
|
| 630 |
+
)
|
| 631 |
+
return preds
|
| 632 |
+
|
| 633 |
+
@torch.no_grad()
|
| 634 |
+
def sample_diffusion(self, protein_pos, protein_v, batch_protein,
|
| 635 |
+
init_ligand_pos, init_ligand_v, batch_ligand,
|
| 636 |
+
num_steps=None, center_pos_mode=None, pos_only=False):
|
| 637 |
+
|
| 638 |
+
if num_steps is None:
|
| 639 |
+
num_steps = self.num_timesteps
|
| 640 |
+
num_graphs = batch_protein.max().item() + 1
|
| 641 |
+
|
| 642 |
+
protein_pos, init_ligand_pos, offset = center_pos(
|
| 643 |
+
protein_pos, init_ligand_pos, batch_protein, batch_ligand, mode=center_pos_mode)
|
| 644 |
+
|
| 645 |
+
pos_traj, v_traj = [], []
|
| 646 |
+
v0_pred_traj, vt_pred_traj = [], []
|
| 647 |
+
ligand_pos, ligand_v = init_ligand_pos, init_ligand_v
|
| 648 |
+
# time sequence
|
| 649 |
+
time_seq = list(reversed(range(self.num_timesteps - num_steps, self.num_timesteps)))
|
| 650 |
+
for i in tqdm(time_seq, desc='sampling', total=len(time_seq)):
|
| 651 |
+
t = torch.full(size=(num_graphs,), fill_value=i, dtype=torch.long, device=protein_pos.device)
|
| 652 |
+
preds = self(
|
| 653 |
+
protein_pos=protein_pos,
|
| 654 |
+
protein_v=protein_v,
|
| 655 |
+
batch_protein=batch_protein,
|
| 656 |
+
|
| 657 |
+
init_ligand_pos=ligand_pos,
|
| 658 |
+
init_ligand_v=ligand_v,
|
| 659 |
+
batch_ligand=batch_ligand,
|
| 660 |
+
time_step=t
|
| 661 |
+
)
|
| 662 |
+
# Compute posterior mean and variance
|
| 663 |
+
if self.model_mean_type == 'noise':
|
| 664 |
+
pred_pos_noise = preds['pred_ligand_pos'] - ligand_pos
|
| 665 |
+
pos0_from_e = self._predict_x0_from_eps(xt=ligand_pos, eps=pred_pos_noise, t=t, batch=batch_ligand)
|
| 666 |
+
v0_from_e = preds['pred_ligand_v']
|
| 667 |
+
elif self.model_mean_type == 'C0':
|
| 668 |
+
pos0_from_e = preds['pred_ligand_pos']
|
| 669 |
+
v0_from_e = preds['pred_ligand_v']
|
| 670 |
+
else:
|
| 671 |
+
raise ValueError
|
| 672 |
+
|
| 673 |
+
pos_model_mean = self.q_pos_posterior(x0=pos0_from_e, xt=ligand_pos, t=t, batch=batch_ligand)
|
| 674 |
+
pos_log_variance = extract(self.posterior_logvar, t, batch_ligand)
|
| 675 |
+
# no noise when t == 0
|
| 676 |
+
nonzero_mask = (1 - (t == 0).float())[batch_ligand].unsqueeze(-1)
|
| 677 |
+
ligand_pos_next = pos_model_mean + nonzero_mask * (0.5 * pos_log_variance).exp() * torch.randn_like(
|
| 678 |
+
ligand_pos)
|
| 679 |
+
ligand_pos = ligand_pos_next
|
| 680 |
+
|
| 681 |
+
if not pos_only:
|
| 682 |
+
log_ligand_v_recon = F.log_softmax(v0_from_e, dim=-1)
|
| 683 |
+
log_ligand_v = index_to_log_onehot(ligand_v, self.num_classes)
|
| 684 |
+
log_model_prob = self.q_v_posterior(log_ligand_v_recon, log_ligand_v, t, batch_ligand)
|
| 685 |
+
ligand_v_next = log_sample_categorical(log_model_prob)
|
| 686 |
+
|
| 687 |
+
v0_pred_traj.append(log_ligand_v_recon.clone().cpu())
|
| 688 |
+
vt_pred_traj.append(log_model_prob.clone().cpu())
|
| 689 |
+
ligand_v = ligand_v_next
|
| 690 |
+
|
| 691 |
+
ori_ligand_pos = ligand_pos + offset[batch_ligand]
|
| 692 |
+
pos_traj.append(ori_ligand_pos.clone().cpu())
|
| 693 |
+
v_traj.append(ligand_v.clone().cpu())
|
| 694 |
+
|
| 695 |
+
ligand_pos = ligand_pos + offset[batch_ligand]
|
| 696 |
+
return {
|
| 697 |
+
'pos': ligand_pos,
|
| 698 |
+
'v': ligand_v,
|
| 699 |
+
'pos_traj': pos_traj,
|
| 700 |
+
'v_traj': v_traj,
|
| 701 |
+
'v0_traj': v0_pred_traj,
|
| 702 |
+
'vt_traj': vt_pred_traj
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def extract(coef, t, batch):
|
| 707 |
+
out = coef[t][batch]
|
| 708 |
+
return out.unsqueeze(-1)
|
models/property_pred/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TargetDiff property prediction models."""
|
| 2 |
+
|
models/property_pred/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (236 Bytes). View file
|
|
|
models/property_pred/__pycache__/prop_egnn.cpython-311.pyc
ADDED
|
Binary file (5.24 kB). View file
|
|
|
models/property_pred/__pycache__/prop_model.cpython-311.pyc
ADDED
|
Binary file (9.78 kB). View file
|
|
|
models/property_pred/prop_egnn.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
from torch_scatter import scatter_sum
|
| 4 |
+
from torch_geometric.nn import radius_graph, knn_graph
|
| 5 |
+
from ..common import GaussianSmearing, MLP
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class EnBaseLayer(nn.Module):
|
| 9 |
+
def __init__(self, hidden_dim, edge_feat_dim, num_r_gaussian, update_x=True, act_fn='relu', norm=False):
|
| 10 |
+
super().__init__()
|
| 11 |
+
self.r_min = 0.
|
| 12 |
+
self.r_max = 10. ** 2
|
| 13 |
+
self.hidden_dim = hidden_dim
|
| 14 |
+
self.num_r_gaussian = num_r_gaussian
|
| 15 |
+
self.edge_feat_dim = edge_feat_dim
|
| 16 |
+
self.update_x = update_x
|
| 17 |
+
self.act_fn = act_fn
|
| 18 |
+
self.norm = norm
|
| 19 |
+
if num_r_gaussian > 1:
|
| 20 |
+
self.r_expansion = GaussianSmearing(self.r_min, self.r_max, num_gaussians=num_r_gaussian, fixed_offset=False)
|
| 21 |
+
self.edge_mlp = MLP(2 * hidden_dim + edge_feat_dim + num_r_gaussian, hidden_dim, hidden_dim,
|
| 22 |
+
num_layer=2, norm=norm, act_fn=act_fn, act_last=True)
|
| 23 |
+
self.edge_inf = nn.Sequential(nn.Linear(hidden_dim, 1), nn.Sigmoid())
|
| 24 |
+
if self.update_x:
|
| 25 |
+
self.x_mlp = MLP(hidden_dim, 1, hidden_dim, num_layer=2, norm=norm, act_fn=act_fn)
|
| 26 |
+
self.node_mlp = MLP(2 * hidden_dim, hidden_dim, hidden_dim, num_layer=2, norm=norm, act_fn=act_fn)
|
| 27 |
+
|
| 28 |
+
def forward(self, h, edge_index, edge_attr):
|
| 29 |
+
dst, src = edge_index
|
| 30 |
+
hi, hj = h[dst], h[src]
|
| 31 |
+
# \phi_e in Eq(3)
|
| 32 |
+
mij = self.edge_mlp(torch.cat([edge_attr, hi, hj], -1))
|
| 33 |
+
eij = self.edge_inf(mij)
|
| 34 |
+
mi = scatter_sum(mij * eij, dst, dim=0, dim_size=h.shape[0])
|
| 35 |
+
|
| 36 |
+
# h update in Eq(6)
|
| 37 |
+
# h = h + self.node_mlp(torch.cat([mi, h], -1))
|
| 38 |
+
output = self.node_mlp(torch.cat([mi, h], -1))
|
| 39 |
+
# if self.update_x:
|
| 40 |
+
# # x update in Eq(4)
|
| 41 |
+
# xi, xj = x[dst], x[src]
|
| 42 |
+
# delta_x = scatter_sum((xi - xj) * self.x_mlp(mij), dst, dim=0)
|
| 43 |
+
# x = x + delta_x
|
| 44 |
+
|
| 45 |
+
return output
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class EnEquiEncoder(nn.Module):
|
| 49 |
+
def __init__(self, num_layers, hidden_dim, edge_feat_dim, num_r_gaussian, k=32, cutoff=10.0,
|
| 50 |
+
update_x=True, act_fn='relu', norm=False):
|
| 51 |
+
super().__init__()
|
| 52 |
+
# Build the network
|
| 53 |
+
self.num_layers = num_layers
|
| 54 |
+
self.hidden_dim = hidden_dim
|
| 55 |
+
self.edge_feat_dim = edge_feat_dim
|
| 56 |
+
self.num_r_gaussian = num_r_gaussian
|
| 57 |
+
self.update_x = update_x
|
| 58 |
+
self.act_fn = act_fn
|
| 59 |
+
self.norm = norm
|
| 60 |
+
self.k = k
|
| 61 |
+
self.cutoff = cutoff
|
| 62 |
+
self.distance_expansion = GaussianSmearing(stop=cutoff, num_gaussians=num_r_gaussian, fixed_offset=False)
|
| 63 |
+
self.net = self._build_network()
|
| 64 |
+
|
| 65 |
+
def _build_network(self):
|
| 66 |
+
# Equivariant layers
|
| 67 |
+
layers = []
|
| 68 |
+
for l_idx in range(self.num_layers):
|
| 69 |
+
layer = EnBaseLayer(self.hidden_dim, self.edge_feat_dim, self.num_r_gaussian,
|
| 70 |
+
update_x=self.update_x, act_fn=self.act_fn, norm=self.norm)
|
| 71 |
+
layers.append(layer)
|
| 72 |
+
return nn.ModuleList(layers)
|
| 73 |
+
|
| 74 |
+
def forward(self, node_attr, pos, batch):
|
| 75 |
+
# edge_index = radius_graph(pos, self.cutoff, batch=batch, loop=False)
|
| 76 |
+
edge_index = knn_graph(pos, k=self.k, batch=batch, flow='target_to_source')
|
| 77 |
+
edge_length = torch.norm(pos[edge_index[0]] - pos[edge_index[1]], dim=1)
|
| 78 |
+
edge_attr = self.distance_expansion(edge_length)
|
| 79 |
+
|
| 80 |
+
h = node_attr
|
| 81 |
+
for interaction in self.net:
|
| 82 |
+
h = h + interaction(h, edge_index, edge_attr)
|
| 83 |
+
return h
|
models/property_pred/prop_model.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import torch.nn.functional as F
|
| 4 |
+
from torch_scatter import scatter
|
| 5 |
+
|
| 6 |
+
from .prop_egnn import EnEquiEncoder
|
| 7 |
+
from ..common import compose_context_prop, ShiftedSoftplus
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def get_encoder(config):
|
| 11 |
+
if config.name == 'egnn' or config.name == 'egnn_enc':
|
| 12 |
+
net = EnEquiEncoder(
|
| 13 |
+
num_layers=config.num_layers,
|
| 14 |
+
edge_feat_dim=config.edge_dim,
|
| 15 |
+
hidden_dim=config.hidden_dim,
|
| 16 |
+
num_r_gaussian=config.num_r_gaussian,
|
| 17 |
+
act_fn=config.act_fn,
|
| 18 |
+
norm=config.norm,
|
| 19 |
+
update_x=False,
|
| 20 |
+
k=config.knn,
|
| 21 |
+
cutoff=config.cutoff,
|
| 22 |
+
)
|
| 23 |
+
else:
|
| 24 |
+
raise ValueError(config.name)
|
| 25 |
+
return net
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class PropPredNet(nn.Module):
|
| 29 |
+
def __init__(self, config, protein_atom_feature_dim, ligand_atom_feature_dim, output_dim=3):
|
| 30 |
+
super(PropPredNet, self).__init__()
|
| 31 |
+
self.config = config
|
| 32 |
+
self.hidden_dim = config.hidden_channels
|
| 33 |
+
self.output_dim = output_dim
|
| 34 |
+
self.protein_atom_emb = nn.Linear(protein_atom_feature_dim, self.hidden_dim)
|
| 35 |
+
self.ligand_atom_emb = nn.Linear(ligand_atom_feature_dim, self.hidden_dim)
|
| 36 |
+
|
| 37 |
+
# self.mean = target_mean
|
| 38 |
+
# self.std = target_std
|
| 39 |
+
# self.register_buffer('target_mean', target_mean)
|
| 40 |
+
# self.register_buffer('target_std', target_std)
|
| 41 |
+
self.encoder = get_encoder(config.encoder)
|
| 42 |
+
self.out_block = nn.Sequential(
|
| 43 |
+
nn.Linear(self.hidden_dim, self.hidden_dim),
|
| 44 |
+
ShiftedSoftplus(),
|
| 45 |
+
nn.Linear(self.hidden_dim, output_dim),
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def forward(self, protein_pos, protein_atom_feature, ligand_pos, ligand_atom_feature, batch_protein, batch_ligand,
|
| 49 |
+
output_kind):
|
| 50 |
+
h_protein = self.protein_atom_emb(protein_atom_feature)
|
| 51 |
+
h_ligand = self.ligand_atom_emb(ligand_atom_feature)
|
| 52 |
+
|
| 53 |
+
h_ctx, pos_ctx, batch_ctx = compose_context_prop(
|
| 54 |
+
h_protein=h_protein,
|
| 55 |
+
h_ligand=h_ligand,
|
| 56 |
+
pos_protein=protein_pos,
|
| 57 |
+
pos_ligand=ligand_pos,
|
| 58 |
+
batch_protein=batch_protein,
|
| 59 |
+
batch_ligand=batch_ligand,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
h_ctx = self.encoder(
|
| 63 |
+
node_attr=h_ctx,
|
| 64 |
+
pos=pos_ctx,
|
| 65 |
+
batch=batch_ctx,
|
| 66 |
+
) # (N_p+N_l, H)
|
| 67 |
+
|
| 68 |
+
# Aggregate messages
|
| 69 |
+
pre_out = scatter(h_ctx, index=batch_ctx, dim=0, reduce='sum') # (N, F)
|
| 70 |
+
output = self.out_block(pre_out) # (N, C)
|
| 71 |
+
if output_kind is not None:
|
| 72 |
+
output_mask = F.one_hot(output_kind - 1, self.output_dim)
|
| 73 |
+
output = torch.sum(output * output_mask, dim=-1, keepdim=True)
|
| 74 |
+
return output
|
| 75 |
+
|
| 76 |
+
def get_loss(self, batch, pos_noise_std, return_pred=False):
|
| 77 |
+
protein_noise = torch.randn_like(batch.protein_pos) * pos_noise_std
|
| 78 |
+
ligand_noise = torch.randn_like(batch.ligand_pos) * pos_noise_std
|
| 79 |
+
pred = self(
|
| 80 |
+
protein_pos=batch.protein_pos + protein_noise,
|
| 81 |
+
protein_atom_feature=batch.protein_atom_feature.float(),
|
| 82 |
+
ligand_pos=batch.ligand_pos + ligand_noise,
|
| 83 |
+
ligand_atom_feature=batch.ligand_atom_feature_full.float(),
|
| 84 |
+
batch_protein=batch.protein_element_batch,
|
| 85 |
+
batch_ligand=batch.ligand_element_batch,
|
| 86 |
+
output_kind=batch.kind,
|
| 87 |
+
# output_kind=None
|
| 88 |
+
)
|
| 89 |
+
# pred = pred * y_std + y_mean
|
| 90 |
+
loss_func = nn.MSELoss()
|
| 91 |
+
loss = loss_func(pred.view(-1), batch.y)
|
| 92 |
+
if return_pred:
|
| 93 |
+
return loss, pred
|
| 94 |
+
else:
|
| 95 |
+
return loss
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class PropPredNetEnc(nn.Module):
|
| 99 |
+
def __init__(self, config, protein_atom_feature_dim, ligand_atom_feature_dim,
|
| 100 |
+
enc_ligand_dim, enc_node_dim, enc_graph_dim, enc_feature_type=None, output_dim=1):
|
| 101 |
+
super(PropPredNetEnc, self).__init__()
|
| 102 |
+
self.config = config
|
| 103 |
+
self.hidden_dim = config.hidden_channels
|
| 104 |
+
self.output_dim = output_dim
|
| 105 |
+
self.enc_ligand_dim = enc_ligand_dim
|
| 106 |
+
self.enc_node_dim = enc_node_dim
|
| 107 |
+
self.enc_graph_dim = enc_graph_dim
|
| 108 |
+
self.enc_feature_type = enc_feature_type
|
| 109 |
+
|
| 110 |
+
self.protein_atom_emb = nn.Linear(protein_atom_feature_dim, self.hidden_dim)
|
| 111 |
+
self.ligand_atom_emb = nn.Linear(ligand_atom_feature_dim + enc_ligand_dim, self.hidden_dim)
|
| 112 |
+
# self.mean = target_mean
|
| 113 |
+
# self.std = target_std
|
| 114 |
+
# self.register_buffer('target_mean', target_mean)
|
| 115 |
+
# self.register_buffer('target_std', target_std)
|
| 116 |
+
self.encoder = get_encoder(config.encoder)
|
| 117 |
+
if self.enc_node_dim > 0:
|
| 118 |
+
self.enc_node_layer = nn.Sequential(
|
| 119 |
+
nn.Linear(self.hidden_dim + self.enc_node_dim, self.hidden_dim),
|
| 120 |
+
nn.ReLU(),
|
| 121 |
+
nn.Linear(self.hidden_dim, self.hidden_dim),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
self.out_block = nn.Sequential(
|
| 125 |
+
nn.Linear(self.hidden_dim + self.enc_graph_dim, self.hidden_dim),
|
| 126 |
+
ShiftedSoftplus(),
|
| 127 |
+
nn.Linear(self.hidden_dim, output_dim),
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
def forward(self, protein_pos, protein_atom_feature, ligand_pos, ligand_atom_feature, batch_protein, batch_ligand,
|
| 131 |
+
output_kind, enc_ligand_feature, enc_node_feature, enc_graph_feature):
|
| 132 |
+
h_protein = self.protein_atom_emb(protein_atom_feature)
|
| 133 |
+
if enc_ligand_feature is not None:
|
| 134 |
+
ligand_atom_feature = torch.cat([ligand_atom_feature, enc_ligand_feature], dim=-1)
|
| 135 |
+
h_ligand = self.ligand_atom_emb(ligand_atom_feature)
|
| 136 |
+
|
| 137 |
+
h_ctx, pos_ctx, batch_ctx = compose_context_prop(
|
| 138 |
+
h_protein=h_protein,
|
| 139 |
+
h_ligand=h_ligand,
|
| 140 |
+
pos_protein=protein_pos,
|
| 141 |
+
pos_ligand=ligand_pos,
|
| 142 |
+
batch_protein=batch_protein,
|
| 143 |
+
batch_ligand=batch_ligand,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
h_ctx = self.encoder(
|
| 147 |
+
node_attr=h_ctx,
|
| 148 |
+
pos=pos_ctx,
|
| 149 |
+
batch=batch_ctx,
|
| 150 |
+
) # (N_p+N_l, H)
|
| 151 |
+
|
| 152 |
+
if enc_node_feature is not None:
|
| 153 |
+
h_ctx = torch.cat([h_ctx, enc_node_feature], dim=-1)
|
| 154 |
+
h_ctx = self.enc_node_layer(h_ctx)
|
| 155 |
+
|
| 156 |
+
# Aggregate messages
|
| 157 |
+
pre_out = scatter(h_ctx, index=batch_ctx, dim=0, reduce='sum') # (N, F)
|
| 158 |
+
if enc_graph_feature is not None:
|
| 159 |
+
pre_out = torch.cat([pre_out, enc_graph_feature], dim=-1)
|
| 160 |
+
|
| 161 |
+
output = self.out_block(pre_out) # (N, C)
|
| 162 |
+
if output_kind is not None:
|
| 163 |
+
output_mask = F.one_hot(output_kind - 1, self.output_dim)
|
| 164 |
+
output = torch.sum(output * output_mask, dim=-1, keepdim=True)
|
| 165 |
+
return output
|
| 166 |
+
|
| 167 |
+
def get_loss(self, batch, pos_noise_std, return_pred=False):
|
| 168 |
+
protein_noise = torch.randn_like(batch.protein_pos) * pos_noise_std
|
| 169 |
+
ligand_noise = torch.randn_like(batch.ligand_pos) * pos_noise_std
|
| 170 |
+
|
| 171 |
+
# add features
|
| 172 |
+
enc_ligand_feature, enc_node_feature, enc_graph_feature = None, None, None
|
| 173 |
+
if self.enc_feature_type == 'nll_all':
|
| 174 |
+
enc_graph_feature = batch.nll_all # [num_graphs, 22]
|
| 175 |
+
elif self.enc_feature_type == 'nll':
|
| 176 |
+
enc_graph_feature = batch.nll # [num_graphs, 20]
|
| 177 |
+
elif self.enc_feature_type == 'final_h':
|
| 178 |
+
enc_node_feature = batch.final_h # [num_pl_atoms, 128]
|
| 179 |
+
elif self.enc_feature_type == 'pred_ligand_v':
|
| 180 |
+
enc_ligand_feature = batch.pred_ligand_v # [num_l_atoms, 13]
|
| 181 |
+
elif self.enc_feature_type == 'pred_v_entropy_pre':
|
| 182 |
+
enc_ligand_feature = batch.pred_v_entropy # [num_l_atoms, 1]
|
| 183 |
+
elif self.enc_feature_type == 'pred_v_entropy_post':
|
| 184 |
+
enc_graph_feature = scatter(batch.pred_v_entropy, index=batch.ligand_element_batch, dim=0, reduce='sum') # [num_graphs, 1]
|
| 185 |
+
elif self.enc_feature_type == 'full':
|
| 186 |
+
enc_graph_feature = torch.cat(
|
| 187 |
+
[batch.nll_all, scatter(batch.pred_v_entropy, index=batch.ligand_element_batch, dim=0, reduce='sum')], dim=-1)
|
| 188 |
+
enc_node_feature = batch.final_h
|
| 189 |
+
enc_ligand_feature = torch.cat([batch.pred_ligand_v, batch.pred_v_entropy], -1)
|
| 190 |
+
else:
|
| 191 |
+
raise NotImplementedError
|
| 192 |
+
|
| 193 |
+
pred = self(
|
| 194 |
+
protein_pos=batch.protein_pos + protein_noise,
|
| 195 |
+
protein_atom_feature=batch.protein_atom_feature.float(),
|
| 196 |
+
ligand_pos=batch.ligand_pos + ligand_noise,
|
| 197 |
+
ligand_atom_feature=batch.ligand_atom_feature_full.float(),
|
| 198 |
+
batch_protein=batch.protein_element_batch,
|
| 199 |
+
batch_ligand=batch.ligand_element_batch,
|
| 200 |
+
output_kind=batch.kind,
|
| 201 |
+
# output_kind=None,
|
| 202 |
+
enc_ligand_feature=enc_ligand_feature,
|
| 203 |
+
enc_node_feature=enc_node_feature,
|
| 204 |
+
enc_graph_feature=enc_graph_feature
|
| 205 |
+
)
|
| 206 |
+
# pred = pred * y_std + y_mean
|
| 207 |
+
loss_func = nn.MSELoss()
|
| 208 |
+
loss = loss_func(pred.view(-1), batch.y)
|
| 209 |
+
if return_pred:
|
| 210 |
+
return loss, pred
|
| 211 |
+
else:
|
| 212 |
+
return loss
|
models/uni_transformer.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
from torch_geometric.nn import radius_graph, knn_graph
|
| 6 |
+
from torch_scatter import scatter_softmax, scatter_sum
|
| 7 |
+
|
| 8 |
+
from .common import GaussianSmearing, MLP, batch_hybrid_edge_connection, outer_product
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class BaseX2HAttLayer(nn.Module):
|
| 12 |
+
def __init__(self, input_dim, hidden_dim, output_dim, n_heads, edge_feat_dim, r_feat_dim,
|
| 13 |
+
act_fn='relu', norm=True, ew_net_type='r', out_fc=True):
|
| 14 |
+
super().__init__()
|
| 15 |
+
self.input_dim = input_dim
|
| 16 |
+
self.hidden_dim = hidden_dim
|
| 17 |
+
self.output_dim = output_dim
|
| 18 |
+
self.n_heads = n_heads
|
| 19 |
+
self.act_fn = act_fn
|
| 20 |
+
self.edge_feat_dim = edge_feat_dim
|
| 21 |
+
self.r_feat_dim = r_feat_dim
|
| 22 |
+
self.ew_net_type = ew_net_type
|
| 23 |
+
self.out_fc = out_fc
|
| 24 |
+
|
| 25 |
+
# attention key func
|
| 26 |
+
kv_input_dim = input_dim * 2 + edge_feat_dim + r_feat_dim
|
| 27 |
+
self.hk_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 28 |
+
|
| 29 |
+
# attention value func
|
| 30 |
+
self.hv_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 31 |
+
|
| 32 |
+
# attention query func
|
| 33 |
+
self.hq_func = MLP(input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 34 |
+
if ew_net_type == 'r':
|
| 35 |
+
self.ew_net = nn.Sequential(nn.Linear(r_feat_dim, 1), nn.Sigmoid())
|
| 36 |
+
elif ew_net_type == 'm':
|
| 37 |
+
self.ew_net = nn.Sequential(nn.Linear(output_dim, 1), nn.Sigmoid())
|
| 38 |
+
|
| 39 |
+
if self.out_fc:
|
| 40 |
+
self.node_output = MLP(2 * hidden_dim, hidden_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 41 |
+
|
| 42 |
+
def forward(self, h, r_feat, edge_feat, edge_index, e_w=None):
|
| 43 |
+
N = h.size(0)
|
| 44 |
+
src, dst = edge_index
|
| 45 |
+
hi, hj = h[dst], h[src]
|
| 46 |
+
|
| 47 |
+
# multi-head attention
|
| 48 |
+
# decide inputs of k_func and v_func
|
| 49 |
+
kv_input = torch.cat([r_feat, hi, hj], -1)
|
| 50 |
+
if edge_feat is not None:
|
| 51 |
+
kv_input = torch.cat([edge_feat, kv_input], -1)
|
| 52 |
+
|
| 53 |
+
# compute k
|
| 54 |
+
k = self.hk_func(kv_input).view(-1, self.n_heads, self.output_dim // self.n_heads)
|
| 55 |
+
# compute v
|
| 56 |
+
v = self.hv_func(kv_input)
|
| 57 |
+
|
| 58 |
+
if self.ew_net_type == 'r':
|
| 59 |
+
e_w = self.ew_net(r_feat)
|
| 60 |
+
elif self.ew_net_type == 'm':
|
| 61 |
+
e_w = self.ew_net(v[..., :self.hidden_dim])
|
| 62 |
+
elif e_w is not None:
|
| 63 |
+
e_w = e_w.view(-1, 1)
|
| 64 |
+
else:
|
| 65 |
+
e_w = 1.
|
| 66 |
+
v = v * e_w
|
| 67 |
+
v = v.view(-1, self.n_heads, self.output_dim // self.n_heads)
|
| 68 |
+
|
| 69 |
+
# compute q
|
| 70 |
+
q = self.hq_func(h).view(-1, self.n_heads, self.output_dim // self.n_heads)
|
| 71 |
+
|
| 72 |
+
# compute attention weights
|
| 73 |
+
alpha = scatter_softmax((q[dst] * k / np.sqrt(k.shape[-1])).sum(-1), dst, dim=0,
|
| 74 |
+
dim_size=N) # [num_edges, n_heads]
|
| 75 |
+
|
| 76 |
+
# perform attention-weighted message-passing
|
| 77 |
+
m = alpha.unsqueeze(-1) * v # (E, heads, H_per_head)
|
| 78 |
+
output = scatter_sum(m, dst, dim=0, dim_size=N) # (N, heads, H_per_head)
|
| 79 |
+
output = output.view(-1, self.output_dim)
|
| 80 |
+
if self.out_fc:
|
| 81 |
+
output = self.node_output(torch.cat([output, h], -1))
|
| 82 |
+
|
| 83 |
+
output = output + h
|
| 84 |
+
return output
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class BaseH2XAttLayer(nn.Module):
|
| 88 |
+
def __init__(self, input_dim, hidden_dim, output_dim, n_heads, edge_feat_dim, r_feat_dim,
|
| 89 |
+
act_fn='relu', norm=True, ew_net_type='r'):
|
| 90 |
+
super().__init__()
|
| 91 |
+
self.input_dim = input_dim
|
| 92 |
+
self.hidden_dim = hidden_dim
|
| 93 |
+
self.output_dim = output_dim
|
| 94 |
+
self.n_heads = n_heads
|
| 95 |
+
self.edge_feat_dim = edge_feat_dim
|
| 96 |
+
self.r_feat_dim = r_feat_dim
|
| 97 |
+
self.act_fn = act_fn
|
| 98 |
+
self.ew_net_type = ew_net_type
|
| 99 |
+
|
| 100 |
+
kv_input_dim = input_dim * 2 + edge_feat_dim + r_feat_dim
|
| 101 |
+
|
| 102 |
+
self.xk_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 103 |
+
self.xv_func = MLP(kv_input_dim, self.n_heads, hidden_dim, norm=norm, act_fn=act_fn)
|
| 104 |
+
self.xq_func = MLP(input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn)
|
| 105 |
+
if ew_net_type == 'r':
|
| 106 |
+
self.ew_net = nn.Sequential(nn.Linear(r_feat_dim, 1), nn.Sigmoid())
|
| 107 |
+
|
| 108 |
+
def forward(self, h, rel_x, r_feat, edge_feat, edge_index, e_w=None):
|
| 109 |
+
N = h.size(0)
|
| 110 |
+
src, dst = edge_index
|
| 111 |
+
hi, hj = h[dst], h[src]
|
| 112 |
+
|
| 113 |
+
# multi-head attention
|
| 114 |
+
# decide inputs of k_func and v_func
|
| 115 |
+
kv_input = torch.cat([r_feat, hi, hj], -1)
|
| 116 |
+
if edge_feat is not None:
|
| 117 |
+
kv_input = torch.cat([edge_feat, kv_input], -1)
|
| 118 |
+
|
| 119 |
+
k = self.xk_func(kv_input).view(-1, self.n_heads, self.output_dim // self.n_heads)
|
| 120 |
+
v = self.xv_func(kv_input)
|
| 121 |
+
if self.ew_net_type == 'r':
|
| 122 |
+
e_w = self.ew_net(r_feat)
|
| 123 |
+
elif self.ew_net_type == 'm':
|
| 124 |
+
e_w = 1.
|
| 125 |
+
elif e_w is not None:
|
| 126 |
+
e_w = e_w.view(-1, 1)
|
| 127 |
+
else:
|
| 128 |
+
e_w = 1.
|
| 129 |
+
v = v * e_w
|
| 130 |
+
|
| 131 |
+
v = v.unsqueeze(-1) * rel_x.unsqueeze(1) # (xi - xj) [n_edges, n_heads, 3]
|
| 132 |
+
q = self.xq_func(h).view(-1, self.n_heads, self.output_dim // self.n_heads)
|
| 133 |
+
|
| 134 |
+
# Compute attention weights
|
| 135 |
+
alpha = scatter_softmax((q[dst] * k / np.sqrt(k.shape[-1])).sum(-1), dst, dim=0, dim_size=N) # (E, heads)
|
| 136 |
+
|
| 137 |
+
# Perform attention-weighted message-passing
|
| 138 |
+
m = alpha.unsqueeze(-1) * v # (E, heads, 3)
|
| 139 |
+
output = scatter_sum(m, dst, dim=0, dim_size=N) # (N, heads, 3)
|
| 140 |
+
return output.mean(1) # [num_nodes, 3]
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class AttentionLayerO2TwoUpdateNodeGeneral(nn.Module):
|
| 144 |
+
def __init__(self, hidden_dim, n_heads, num_r_gaussian, edge_feat_dim, act_fn='relu', norm=True,
|
| 145 |
+
num_x2h=1, num_h2x=1, r_min=0., r_max=10., num_node_types=8,
|
| 146 |
+
ew_net_type='r', x2h_out_fc=True, sync_twoup=False):
|
| 147 |
+
super().__init__()
|
| 148 |
+
self.hidden_dim = hidden_dim
|
| 149 |
+
self.n_heads = n_heads
|
| 150 |
+
self.edge_feat_dim = edge_feat_dim
|
| 151 |
+
self.num_r_gaussian = num_r_gaussian
|
| 152 |
+
self.norm = norm
|
| 153 |
+
self.act_fn = act_fn
|
| 154 |
+
self.num_x2h = num_x2h
|
| 155 |
+
self.num_h2x = num_h2x
|
| 156 |
+
self.r_min, self.r_max = r_min, r_max
|
| 157 |
+
self.num_node_types = num_node_types
|
| 158 |
+
self.ew_net_type = ew_net_type
|
| 159 |
+
self.x2h_out_fc = x2h_out_fc
|
| 160 |
+
self.sync_twoup = sync_twoup
|
| 161 |
+
|
| 162 |
+
self.distance_expansion = GaussianSmearing(self.r_min, self.r_max, num_gaussians=num_r_gaussian)
|
| 163 |
+
|
| 164 |
+
self.x2h_layers = nn.ModuleList()
|
| 165 |
+
for i in range(self.num_x2h):
|
| 166 |
+
self.x2h_layers.append(
|
| 167 |
+
BaseX2HAttLayer(hidden_dim, hidden_dim, hidden_dim, n_heads, edge_feat_dim,
|
| 168 |
+
r_feat_dim=num_r_gaussian * 4,
|
| 169 |
+
act_fn=act_fn, norm=norm,
|
| 170 |
+
ew_net_type=self.ew_net_type, out_fc=self.x2h_out_fc)
|
| 171 |
+
)
|
| 172 |
+
self.h2x_layers = nn.ModuleList()
|
| 173 |
+
for i in range(self.num_h2x):
|
| 174 |
+
self.h2x_layers.append(
|
| 175 |
+
BaseH2XAttLayer(hidden_dim, hidden_dim, hidden_dim, n_heads, edge_feat_dim,
|
| 176 |
+
r_feat_dim=num_r_gaussian * 4,
|
| 177 |
+
act_fn=act_fn, norm=norm,
|
| 178 |
+
ew_net_type=self.ew_net_type)
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
def forward(self, h, x, edge_attr, edge_index, mask_ligand, e_w=None, fix_x=False):
|
| 182 |
+
src, dst = edge_index
|
| 183 |
+
if self.edge_feat_dim > 0:
|
| 184 |
+
edge_feat = edge_attr # shape: [#edges_in_batch, #bond_types]
|
| 185 |
+
else:
|
| 186 |
+
edge_feat = None
|
| 187 |
+
|
| 188 |
+
rel_x = x[dst] - x[src]
|
| 189 |
+
dist = torch.norm(rel_x, p=2, dim=-1, keepdim=True)
|
| 190 |
+
|
| 191 |
+
h_in = h
|
| 192 |
+
# 4 separate distance embedding for p-p, p-l, l-p, l-l
|
| 193 |
+
for i in range(self.num_x2h):
|
| 194 |
+
dist_feat = self.distance_expansion(dist)
|
| 195 |
+
dist_feat = outer_product(edge_attr, dist_feat)
|
| 196 |
+
h_out = self.x2h_layers[i](h_in, dist_feat, edge_feat, edge_index, e_w=e_w)
|
| 197 |
+
h_in = h_out
|
| 198 |
+
x2h_out = h_in
|
| 199 |
+
|
| 200 |
+
new_h = h if self.sync_twoup else x2h_out
|
| 201 |
+
for i in range(self.num_h2x):
|
| 202 |
+
dist_feat = self.distance_expansion(dist)
|
| 203 |
+
dist_feat = outer_product(edge_attr, dist_feat)
|
| 204 |
+
delta_x = self.h2x_layers[i](new_h, rel_x, dist_feat, edge_feat, edge_index, e_w=e_w)
|
| 205 |
+
if not fix_x:
|
| 206 |
+
x = x + delta_x * mask_ligand[:, None] # only ligand positions will be updated
|
| 207 |
+
rel_x = x[dst] - x[src]
|
| 208 |
+
dist = torch.norm(rel_x, p=2, dim=-1, keepdim=True)
|
| 209 |
+
|
| 210 |
+
return x2h_out, x
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class UniTransformerO2TwoUpdateGeneral(nn.Module):
|
| 214 |
+
def __init__(self, num_blocks, num_layers, hidden_dim, n_heads=1, k=32,
|
| 215 |
+
num_r_gaussian=50, edge_feat_dim=0, num_node_types=8, act_fn='relu', norm=True,
|
| 216 |
+
cutoff_mode='radius', ew_net_type='r',
|
| 217 |
+
num_init_x2h=1, num_init_h2x=0, num_x2h=1, num_h2x=1, r_max=10., x2h_out_fc=True, sync_twoup=False):
|
| 218 |
+
super().__init__()
|
| 219 |
+
# Build the network
|
| 220 |
+
self.num_blocks = num_blocks
|
| 221 |
+
self.num_layers = num_layers
|
| 222 |
+
self.hidden_dim = hidden_dim
|
| 223 |
+
self.n_heads = n_heads
|
| 224 |
+
self.num_r_gaussian = num_r_gaussian
|
| 225 |
+
self.edge_feat_dim = edge_feat_dim
|
| 226 |
+
self.act_fn = act_fn
|
| 227 |
+
self.norm = norm
|
| 228 |
+
self.num_node_types = num_node_types
|
| 229 |
+
# radius graph / knn graph
|
| 230 |
+
self.cutoff_mode = cutoff_mode # [radius, none]
|
| 231 |
+
self.k = k
|
| 232 |
+
self.ew_net_type = ew_net_type # [r, m, none]
|
| 233 |
+
|
| 234 |
+
self.num_x2h = num_x2h
|
| 235 |
+
self.num_h2x = num_h2x
|
| 236 |
+
self.num_init_x2h = num_init_x2h
|
| 237 |
+
self.num_init_h2x = num_init_h2x
|
| 238 |
+
self.r_max = r_max
|
| 239 |
+
self.x2h_out_fc = x2h_out_fc
|
| 240 |
+
self.sync_twoup = sync_twoup
|
| 241 |
+
self.distance_expansion = GaussianSmearing(0., r_max, num_gaussians=num_r_gaussian)
|
| 242 |
+
if self.ew_net_type == 'global':
|
| 243 |
+
self.edge_pred_layer = MLP(num_r_gaussian, 1, hidden_dim)
|
| 244 |
+
|
| 245 |
+
self.init_h_emb_layer = self._build_init_h_layer()
|
| 246 |
+
self.base_block = self._build_share_blocks()
|
| 247 |
+
|
| 248 |
+
def __repr__(self):
|
| 249 |
+
return f'UniTransformerO2(num_blocks={self.num_blocks}, num_layers={self.num_layers}, n_heads={self.n_heads}, ' \
|
| 250 |
+
f'act_fn={self.act_fn}, norm={self.norm}, cutoff_mode={self.cutoff_mode}, ew_net_type={self.ew_net_type}, ' \
|
| 251 |
+
f'init h emb: {self.init_h_emb_layer.__repr__()} \n' \
|
| 252 |
+
f'base block: {self.base_block.__repr__()} \n' \
|
| 253 |
+
f'edge pred layer: {self.edge_pred_layer.__repr__() if hasattr(self, "edge_pred_layer") else "None"}) '
|
| 254 |
+
|
| 255 |
+
def _build_init_h_layer(self):
|
| 256 |
+
layer = AttentionLayerO2TwoUpdateNodeGeneral(
|
| 257 |
+
self.hidden_dim, self.n_heads, self.num_r_gaussian, self.edge_feat_dim, act_fn=self.act_fn, norm=self.norm,
|
| 258 |
+
num_x2h=self.num_init_x2h, num_h2x=self.num_init_h2x, r_max=self.r_max, num_node_types=self.num_node_types,
|
| 259 |
+
ew_net_type=self.ew_net_type, x2h_out_fc=self.x2h_out_fc, sync_twoup=self.sync_twoup,
|
| 260 |
+
)
|
| 261 |
+
return layer
|
| 262 |
+
|
| 263 |
+
def _build_share_blocks(self):
|
| 264 |
+
# Equivariant layers
|
| 265 |
+
base_block = []
|
| 266 |
+
for l_idx in range(self.num_layers):
|
| 267 |
+
layer = AttentionLayerO2TwoUpdateNodeGeneral(
|
| 268 |
+
self.hidden_dim, self.n_heads, self.num_r_gaussian, self.edge_feat_dim, act_fn=self.act_fn,
|
| 269 |
+
norm=self.norm,
|
| 270 |
+
num_x2h=self.num_x2h, num_h2x=self.num_h2x, r_max=self.r_max, num_node_types=self.num_node_types,
|
| 271 |
+
ew_net_type=self.ew_net_type, x2h_out_fc=self.x2h_out_fc, sync_twoup=self.sync_twoup,
|
| 272 |
+
)
|
| 273 |
+
base_block.append(layer)
|
| 274 |
+
return nn.ModuleList(base_block)
|
| 275 |
+
|
| 276 |
+
def _connect_edge(self, x, mask_ligand, batch):
|
| 277 |
+
if self.cutoff_mode == 'radius':
|
| 278 |
+
edge_index = radius_graph(x, r=self.r, batch=batch, flow='source_to_target')
|
| 279 |
+
elif self.cutoff_mode == 'knn':
|
| 280 |
+
edge_index = knn_graph(x, k=self.k, batch=batch, flow='source_to_target')
|
| 281 |
+
elif self.cutoff_mode == 'hybrid':
|
| 282 |
+
edge_index = batch_hybrid_edge_connection(
|
| 283 |
+
x, k=self.k, mask_ligand=mask_ligand, batch=batch, add_p_index=True)
|
| 284 |
+
else:
|
| 285 |
+
raise ValueError(f'Not supported cutoff mode: {self.cutoff_mode}')
|
| 286 |
+
return edge_index
|
| 287 |
+
|
| 288 |
+
@staticmethod
|
| 289 |
+
def _build_edge_type(edge_index, mask_ligand):
|
| 290 |
+
src, dst = edge_index
|
| 291 |
+
edge_type = torch.zeros(len(src)).to(edge_index)
|
| 292 |
+
n_src = mask_ligand[src] == 1
|
| 293 |
+
n_dst = mask_ligand[dst] == 1
|
| 294 |
+
edge_type[n_src & n_dst] = 0
|
| 295 |
+
edge_type[n_src & ~n_dst] = 1
|
| 296 |
+
edge_type[~n_src & n_dst] = 2
|
| 297 |
+
edge_type[~n_src & ~n_dst] = 3
|
| 298 |
+
edge_type = F.one_hot(edge_type, num_classes=4)
|
| 299 |
+
return edge_type
|
| 300 |
+
|
| 301 |
+
def forward(self, h, x, mask_ligand, batch, return_all=False, fix_x=False):
|
| 302 |
+
|
| 303 |
+
all_x = [x]
|
| 304 |
+
all_h = [h]
|
| 305 |
+
|
| 306 |
+
for b_idx in range(self.num_blocks):
|
| 307 |
+
edge_index = self._connect_edge(x, mask_ligand, batch)
|
| 308 |
+
src, dst = edge_index
|
| 309 |
+
|
| 310 |
+
# edge type (dim: 4)
|
| 311 |
+
edge_type = self._build_edge_type(edge_index, mask_ligand)
|
| 312 |
+
if self.ew_net_type == 'global':
|
| 313 |
+
dist = torch.norm(x[dst] - x[src], p=2, dim=-1, keepdim=True)
|
| 314 |
+
dist_feat = self.distance_expansion(dist)
|
| 315 |
+
logits = self.edge_pred_layer(dist_feat)
|
| 316 |
+
e_w = torch.sigmoid(logits)
|
| 317 |
+
else:
|
| 318 |
+
e_w = None
|
| 319 |
+
|
| 320 |
+
for l_idx, layer in enumerate(self.base_block):
|
| 321 |
+
h, x = layer(h, x, edge_type, edge_index, mask_ligand, e_w=e_w, fix_x=fix_x)
|
| 322 |
+
all_x.append(x)
|
| 323 |
+
all_h.append(h)
|
| 324 |
+
|
| 325 |
+
outputs = {'x': x, 'h': h}
|
| 326 |
+
if return_all:
|
| 327 |
+
outputs.update({'all_x': all_x, 'all_h': all_h})
|
| 328 |
+
return outputs
|
scripts/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TargetDiff example scripts."""
|
| 2 |
+
|
scripts/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (240 Bytes). View file
|
|
|
scripts/__pycache__/sample_diffusion.cpython-311.pyc
ADDED
|
Binary file (13.9 kB). View file
|
|
|
scripts/__pycache__/sample_for_pocket.cpython-311.pyc
ADDED
|
Binary file (7.53 kB). View file
|
|
|
scripts/batch_sample_diffusion.sh
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TOTAL_TASKS=100
|
| 2 |
+
BATCH_SIZE=50
|
| 3 |
+
|
| 4 |
+
if [ $# != 5 ]; then
|
| 5 |
+
echo "Error: 5 arguments required."
|
| 6 |
+
exit 1
|
| 7 |
+
fi
|
| 8 |
+
|
| 9 |
+
CONFIG_FILE=$1
|
| 10 |
+
RESULT_PATH=$2
|
| 11 |
+
NODE_ALL=$3
|
| 12 |
+
NODE_THIS=$4
|
| 13 |
+
START_IDX=$5
|
| 14 |
+
|
| 15 |
+
for ((i=$START_IDX;i<$TOTAL_TASKS;i++)); do
|
| 16 |
+
NODE_TARGET=$(($i % $NODE_ALL))
|
| 17 |
+
if [ $NODE_TARGET == $NODE_THIS ]; then
|
| 18 |
+
echo "Task ${i} assigned to this worker (${NODE_THIS})"
|
| 19 |
+
python -m scripts.sample_diffusion ${CONFIG_FILE} -i ${i} --batch_size ${BATCH_SIZE} --result_path ${RESULT_PATH}
|
| 20 |
+
fi
|
| 21 |
+
done
|
scripts/data_preparation/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TargetDiff data preparation scripts."""
|
| 2 |
+
|
scripts/data_preparation/clean_crossdocked.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import gzip
|
| 4 |
+
import pickle
|
| 5 |
+
import argparse
|
| 6 |
+
from tqdm.auto import tqdm
|
| 7 |
+
|
| 8 |
+
TYPES_FILENAME = 'types/it2_tt_v1.1_completeset_train0.types' # 'types/it2_tt_completeset_train0.types'
|
| 9 |
+
|
| 10 |
+
if __name__ == '__main__':
|
| 11 |
+
parser = argparse.ArgumentParser()
|
| 12 |
+
parser.add_argument('--source', type=str, default='./data/CrossDocked2020')
|
| 13 |
+
parser.add_argument('--dest', type=str, required=True)
|
| 14 |
+
parser.add_argument('--rmsd_thr', type=float, default=1.0)
|
| 15 |
+
args = parser.parse_args()
|
| 16 |
+
|
| 17 |
+
os.makedirs(args.dest, exist_ok=False)
|
| 18 |
+
types_path = os.path.join(args.source, TYPES_FILENAME)
|
| 19 |
+
|
| 20 |
+
index = []
|
| 21 |
+
with open(types_path, 'r') as f:
|
| 22 |
+
for ln in tqdm(f.readlines()):
|
| 23 |
+
_, _, rmsd, protein_fn, ligand_fn, _ = ln.split()
|
| 24 |
+
rmsd = float(rmsd)
|
| 25 |
+
if rmsd > args.rmsd_thr:
|
| 26 |
+
continue
|
| 27 |
+
|
| 28 |
+
ligand_id = int(ligand_fn[ligand_fn.rfind('_')+1:ligand_fn.rfind('.')])
|
| 29 |
+
|
| 30 |
+
protein_fn = protein_fn[:protein_fn.rfind('_')] + '.pdb'
|
| 31 |
+
# For CrossDocked v1.0
|
| 32 |
+
# ligand_raw_fn = ligand_fn[:ligand_fn.rfind('_')] + '.sdf'
|
| 33 |
+
ligand_raw_fn = ligand_fn[:ligand_fn.rfind('_')] + '.sdf.gz'
|
| 34 |
+
protein_path = os.path.join(args.source, protein_fn)
|
| 35 |
+
ligand_raw_path = os.path.join(args.source, ligand_raw_fn)
|
| 36 |
+
if not (os.path.exists(protein_path) and os.path.exists(ligand_raw_path)):
|
| 37 |
+
continue
|
| 38 |
+
|
| 39 |
+
# For CrossDocked v1.0
|
| 40 |
+
# with open(ligand_raw_path, 'r') as f:
|
| 41 |
+
with gzip.open(ligand_raw_path, 'rt') as f:
|
| 42 |
+
ligand_sdf = f.read().split('$$$$\n')[ligand_id]
|
| 43 |
+
ligand_save_fn = ligand_fn[:ligand_fn.rfind('.')] + '.sdf' # include ligand id
|
| 44 |
+
|
| 45 |
+
protein_dest = os.path.join(args.dest, protein_fn)
|
| 46 |
+
ligand_dest = os.path.join(args.dest, ligand_save_fn)
|
| 47 |
+
os.makedirs(os.path.dirname(protein_dest), exist_ok=True)
|
| 48 |
+
os.makedirs(os.path.dirname(ligand_dest), exist_ok=True)
|
| 49 |
+
shutil.copyfile(protein_path, protein_dest)
|
| 50 |
+
with open(ligand_dest, 'w') as f:
|
| 51 |
+
f.write(ligand_sdf)
|
| 52 |
+
|
| 53 |
+
index.append((protein_fn, ligand_save_fn, rmsd))
|
| 54 |
+
|
| 55 |
+
index_path = os.path.join(args.dest, 'index.pkl')
|
| 56 |
+
with open(index_path, 'wb') as f:
|
| 57 |
+
pickle.dump(index, f)
|
| 58 |
+
|
| 59 |
+
print(f'Done processing {len(index)} protein-ligand pairs in total.\n Processed files in {args.dest}.')
|
scripts/data_preparation/extract_pockets.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import multiprocessing as mp
|
| 4 |
+
import pickle
|
| 5 |
+
import shutil
|
| 6 |
+
from functools import partial
|
| 7 |
+
|
| 8 |
+
from tqdm.auto import tqdm
|
| 9 |
+
|
| 10 |
+
from onescience.utils.targetdiff.data import PDBProtein, parse_sdf_file
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_item(item, path):
|
| 14 |
+
pdb_path = os.path.join(path, item[0])
|
| 15 |
+
sdf_path = os.path.join(path, item[1])
|
| 16 |
+
with open(pdb_path, 'r') as f:
|
| 17 |
+
pdb_block = f.read()
|
| 18 |
+
with open(sdf_path, 'r') as f:
|
| 19 |
+
sdf_block = f.read()
|
| 20 |
+
return pdb_block, sdf_block
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def process_item(item, args):
|
| 24 |
+
try:
|
| 25 |
+
pdb_block, sdf_block = load_item(item, args.source)
|
| 26 |
+
protein = PDBProtein(pdb_block)
|
| 27 |
+
# ligand = parse_sdf_block(sdf_block)
|
| 28 |
+
ligand = parse_sdf_file(os.path.join(args.source, item[1]))
|
| 29 |
+
|
| 30 |
+
pdb_block_pocket = protein.residues_to_pdb_block(
|
| 31 |
+
protein.query_residues_ligand(ligand, args.radius)
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
ligand_fn = item[1]
|
| 35 |
+
pocket_fn = ligand_fn[:-4] + '_pocket%d.pdb' % args.radius
|
| 36 |
+
ligand_dest = os.path.join(args.dest, ligand_fn)
|
| 37 |
+
pocket_dest = os.path.join(args.dest, pocket_fn)
|
| 38 |
+
os.makedirs(os.path.dirname(ligand_dest), exist_ok=True)
|
| 39 |
+
|
| 40 |
+
shutil.copyfile(
|
| 41 |
+
src=os.path.join(args.source, ligand_fn),
|
| 42 |
+
dst=os.path.join(args.dest, ligand_fn)
|
| 43 |
+
)
|
| 44 |
+
with open(pocket_dest, 'w') as f:
|
| 45 |
+
f.write(pdb_block_pocket)
|
| 46 |
+
return pocket_fn, ligand_fn, item[0], item[2] # item[0]: original protein filename; item[2]: rmsd.
|
| 47 |
+
except Exception:
|
| 48 |
+
print('Exception occurred.', item)
|
| 49 |
+
return None, item[1], item[0], item[2]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == '__main__':
|
| 53 |
+
parser = argparse.ArgumentParser()
|
| 54 |
+
parser.add_argument('--source', type=str, default='./data/crossdocked_subset')
|
| 55 |
+
parser.add_argument('--dest', type=str, required=True)
|
| 56 |
+
parser.add_argument('--radius', type=int, default=10)
|
| 57 |
+
parser.add_argument('--num_workers', type=int, default=16)
|
| 58 |
+
args = parser.parse_args()
|
| 59 |
+
|
| 60 |
+
os.makedirs(args.dest, exist_ok=False)
|
| 61 |
+
with open(os.path.join(args.source, 'index.pkl'), 'rb') as f:
|
| 62 |
+
index = pickle.load(f)
|
| 63 |
+
|
| 64 |
+
pool = mp.Pool(args.num_workers)
|
| 65 |
+
index_pocket = []
|
| 66 |
+
for item_pocket in tqdm(pool.imap_unordered(partial(process_item, args=args), index), total=len(index)):
|
| 67 |
+
index_pocket.append(item_pocket)
|
| 68 |
+
# index_pocket = pool.map(partial(process_item, args=args), index)
|
| 69 |
+
pool.close()
|
| 70 |
+
|
| 71 |
+
index_path = os.path.join(args.dest, 'index.pkl')
|
| 72 |
+
with open(index_path, 'wb') as f:
|
| 73 |
+
pickle.dump(index_pocket, f)
|
| 74 |
+
|
| 75 |
+
print('Done. %d protein-ligand pairs in total.' % len(index))
|
| 76 |
+
|
scripts/data_preparation/split_pl_dataset.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
import random
|
| 4 |
+
import torch
|
| 5 |
+
from tqdm.auto import tqdm
|
| 6 |
+
|
| 7 |
+
from torch.utils.data import Subset
|
| 8 |
+
from onescience.datapipes.targetdiff.pl_pair_dataset import PocketLigandPairDataset
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def get_chain_name(fn):
|
| 12 |
+
return os.path.basename(fn)[:6]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def get_pdb_name(fn):
|
| 16 |
+
return os.path.basename(fn)[:4]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_unique_pockets(dataset, raw_id, used_pdb, num_pockets):
|
| 20 |
+
# only save first encountered id for unseen pdbs
|
| 21 |
+
unique_id = []
|
| 22 |
+
pdb_visited = set()
|
| 23 |
+
for idx in tqdm(raw_id, 'Filter'):
|
| 24 |
+
pdb_name = get_pdb_name(dataset[idx].ligand_filename)
|
| 25 |
+
if pdb_name not in used_pdb and pdb_name not in pdb_visited:
|
| 26 |
+
unique_id.append(idx)
|
| 27 |
+
pdb_visited.add(pdb_name)
|
| 28 |
+
|
| 29 |
+
print('Number of Pairs: %d' % len(unique_id))
|
| 30 |
+
print('Number of PDBs: %d' % len(pdb_visited))
|
| 31 |
+
|
| 32 |
+
random.Random(args.seed).shuffle(unique_id)
|
| 33 |
+
unique_id = unique_id[:num_pockets]
|
| 34 |
+
print('Number of selected: %d' % len(unique_id))
|
| 35 |
+
return unique_id, pdb_visited.union(used_pdb)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == '__main__':
|
| 39 |
+
parser = argparse.ArgumentParser()
|
| 40 |
+
parser.add_argument('--path', type=str, default='./data/crossdocked_v1.1_rmsd1.0_pocket10')
|
| 41 |
+
parser.add_argument('--dest', type=str, default='./data/crossdocked_pocket10_pose_split.pt')
|
| 42 |
+
parser.add_argument('--fixed_split', type=str, default='./data/split_by_name.pt')
|
| 43 |
+
parser.add_argument('--train', type=int, default=100000)
|
| 44 |
+
parser.add_argument('--val', type=int, default=1000)
|
| 45 |
+
parser.add_argument('--test', type=int, default=20000)
|
| 46 |
+
parser.add_argument('--val_num_pockets', type=int, default=-1)
|
| 47 |
+
parser.add_argument('--test_num_pockets', type=int, default=100)
|
| 48 |
+
parser.add_argument('--seed', type=int, default=2021)
|
| 49 |
+
args = parser.parse_args()
|
| 50 |
+
|
| 51 |
+
dataset = PocketLigandPairDataset(args.path)
|
| 52 |
+
print('Load dataset successfully!')
|
| 53 |
+
if args.fixed_split:
|
| 54 |
+
fixed_split = torch.load(args.fixed_split)
|
| 55 |
+
print('Load fixed split successfully!')
|
| 56 |
+
name_id_dict = {}
|
| 57 |
+
for idx, data in enumerate(tqdm(dataset, desc='Indexing')):
|
| 58 |
+
name_id_dict[data.protein_filename + data.ligand_filename] = idx
|
| 59 |
+
|
| 60 |
+
selected_ids = {'train': [], 'test': []}
|
| 61 |
+
for split in ['train', 'test']:
|
| 62 |
+
print(f'Selecting {split} split...')
|
| 63 |
+
for fn in fixed_split[split]:
|
| 64 |
+
if (fn[0] + fn[1]) in name_id_dict:
|
| 65 |
+
selected_ids[split].append(name_id_dict[fn[0] + fn[1]])
|
| 66 |
+
else:
|
| 67 |
+
print(f'Warning: data with PDB fn {fn[0]} and ligand fn {fn[1]} not found!')
|
| 68 |
+
train_id, val_id, test_id = selected_ids['train'], [], selected_ids['test']
|
| 69 |
+
|
| 70 |
+
else:
|
| 71 |
+
allowed_elements = {1, 6, 7, 8, 9, 15, 16, 17}
|
| 72 |
+
elements = {i: set() for i in range(90)}
|
| 73 |
+
for i, data in enumerate(tqdm(dataset, desc='Filter')):
|
| 74 |
+
for e in data.ligand_element:
|
| 75 |
+
elements[e.item()].add(i)
|
| 76 |
+
|
| 77 |
+
all_id = set(range(len(dataset)))
|
| 78 |
+
blocked_id = set().union(*[
|
| 79 |
+
elements[i] for i in elements.keys() if i not in allowed_elements
|
| 80 |
+
])
|
| 81 |
+
|
| 82 |
+
allowed_id = list(all_id - blocked_id)
|
| 83 |
+
random.Random(args.seed).shuffle(allowed_id)
|
| 84 |
+
print('Allowed: %d' % len(allowed_id))
|
| 85 |
+
|
| 86 |
+
train_id = allowed_id[:args.train]
|
| 87 |
+
train_set = Subset(dataset, indices=train_id)
|
| 88 |
+
train_pdb = {get_pdb_name(d.ligand_filename) for d in tqdm(train_set)}
|
| 89 |
+
print('train pdb: ', train_pdb)
|
| 90 |
+
|
| 91 |
+
if args.val_num_pockets == -1:
|
| 92 |
+
# not group by pocket
|
| 93 |
+
val_id = allowed_id[args.train: args.train + args.val]
|
| 94 |
+
used_pdb = train_pdb
|
| 95 |
+
else:
|
| 96 |
+
raw_val_id = allowed_id[args.train: args.train + args.val]
|
| 97 |
+
val_id, used_pdb = get_unique_pockets(dataset, raw_val_id, train_pdb, args.val_num_pockets)
|
| 98 |
+
|
| 99 |
+
if args.test_num_pockets == -1:
|
| 100 |
+
test_id = allowed_id[args.train + args.val: args.train + args.val + args.test]
|
| 101 |
+
else:
|
| 102 |
+
raw_test_id = allowed_id[args.train + args.val: args.train + args.val + args.test]
|
| 103 |
+
test_id, used_pdb = get_unique_pockets(dataset, raw_test_id, used_pdb, args.test_num_pockets)
|
| 104 |
+
|
| 105 |
+
torch.save({
|
| 106 |
+
'train': train_id,
|
| 107 |
+
'val': val_id,
|
| 108 |
+
'test': test_id,
|
| 109 |
+
}, args.dest)
|
| 110 |
+
|
| 111 |
+
print('Train %d, Validation %d, Test %d.' % (len(train_id), len(val_id), len(test_id)))
|
| 112 |
+
print('Done.')
|
scripts/dock_baseline.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import torch
|
| 4 |
+
from tqdm.auto import tqdm
|
| 5 |
+
from onescience.utils.targetdiff.evaluation.docking_qvina import QVinaDockingTask
|
| 6 |
+
from onescience.utils.targetdiff.evaluation.docking_vina import VinaDockingTask
|
| 7 |
+
import multiprocessing as mp
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def dock_pocket_samples(pocket_samples):
|
| 11 |
+
ligand_fn = pocket_samples[0]['ligand_filename']
|
| 12 |
+
print('Start docking pocket: %s' % ligand_fn)
|
| 13 |
+
pocket_results = []
|
| 14 |
+
for idx, s in enumerate(tqdm(pocket_samples, desc='docking %d' % os.getpid())):
|
| 15 |
+
try:
|
| 16 |
+
if args.docking_mode == 'qvina':
|
| 17 |
+
vina_task = QVinaDockingTask.from_generated_mol(
|
| 18 |
+
s['mol'], s['ligand_filename'], protein_root=args.protein_root, size_factor=args.dock_size_factor)
|
| 19 |
+
vina_results = vina_task.run_sync()
|
| 20 |
+
elif args.docking_mode == 'vina_score':
|
| 21 |
+
vina_task = VinaDockingTask.from_generated_mol(
|
| 22 |
+
s['mol'], s['ligand_filename'], protein_root=args.protein_root)
|
| 23 |
+
score_only_results = vina_task.run(mode='score_only', exhaustiveness=args.exhaustiveness)
|
| 24 |
+
minimize_results = vina_task.run(mode='minimize', exhaustiveness=args.exhaustiveness)
|
| 25 |
+
vina_results = {
|
| 26 |
+
'score_only': score_only_results,
|
| 27 |
+
'minimize': minimize_results
|
| 28 |
+
}
|
| 29 |
+
else:
|
| 30 |
+
raise ValueError
|
| 31 |
+
except:
|
| 32 |
+
print('Error at %d of %s' % (idx, ligand_fn))
|
| 33 |
+
vina_results = None
|
| 34 |
+
pocket_results.append({**s, 'vina': vina_results})
|
| 35 |
+
return pocket_results
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == '__main__':
|
| 39 |
+
parser = argparse.ArgumentParser()
|
| 40 |
+
parser.add_argument('sample_path', type=str)
|
| 41 |
+
parser.add_argument('-o', '--out', type=str, default=None)
|
| 42 |
+
parser.add_argument('-n', '--num_processes', type=int, default=10)
|
| 43 |
+
parser.add_argument('--protein_root', type=str, default='./data/crossdocked_v1.1_rmsd1.0')
|
| 44 |
+
parser.add_argument('--dock_size_factor', type=float, default=None)
|
| 45 |
+
parser.add_argument('--exhaustiveness', type=int, default=16)
|
| 46 |
+
parser.add_argument('--docking_mode', type=str, default='vina_score',
|
| 47 |
+
choices=['none', 'qvina', 'vina_score'])
|
| 48 |
+
args = parser.parse_args()
|
| 49 |
+
|
| 50 |
+
samples = torch.load(args.sample_path)
|
| 51 |
+
with mp.Pool(args.num_processes) as p:
|
| 52 |
+
docked_samples = p.map(dock_pocket_samples, samples)
|
| 53 |
+
if args.out is None:
|
| 54 |
+
dir_name = os.path.dirname(args.sample_path)
|
| 55 |
+
baseline_name = os.path.basename(args.sample_path).split('_')[0]
|
| 56 |
+
out_path = os.path.join(dir_name, baseline_name + '_test_docked.pt')
|
| 57 |
+
else:
|
| 58 |
+
out_path = args.out
|
| 59 |
+
torch.save(docked_samples, out_path)
|
scripts/dock_testset.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from rdkit import Chem
|
| 5 |
+
import torch
|
| 6 |
+
from tqdm.auto import tqdm
|
| 7 |
+
from onescience.utils.targetdiff import misc
|
| 8 |
+
from onescience.utils.targetdiff.evaluation import scoring_func
|
| 9 |
+
from onescience.utils.targetdiff.evaluation.docking_qvina import QVinaDockingTask
|
| 10 |
+
from onescience.datapipes.targetdiff import get_dataset
|
| 11 |
+
from easydict import EasyDict
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
if __name__ == '__main__':
|
| 15 |
+
parser = argparse.ArgumentParser()
|
| 16 |
+
parser.add_argument('-d', '--dataset', type=str, default='./data/crossdocked_v1.1_rmsd1.0_pocket10')
|
| 17 |
+
parser.add_argument('-s', '--split', type=str, default='./data/crossdocked_pocket10_pose_split.pt')
|
| 18 |
+
parser.add_argument('-o', '--out', type=str, default=None)
|
| 19 |
+
parser.add_argument('--protein_root', type=str, default='./data/crossdocked_v1.1_rmsd1.0')
|
| 20 |
+
parser.add_argument('--ligand_root', type=str, default='./data/crossdocked_v1.1_rmsd1.0_pocket10')
|
| 21 |
+
parser.add_argument('--use_uff', type=eval, default=True)
|
| 22 |
+
parser.add_argument('--size_factor', type=float, default=1.2)
|
| 23 |
+
args = parser.parse_args()
|
| 24 |
+
|
| 25 |
+
logger = misc.get_logger('docking')
|
| 26 |
+
logger.info(args)
|
| 27 |
+
|
| 28 |
+
# Load dataset
|
| 29 |
+
dataset, subsets = get_dataset(
|
| 30 |
+
config=EasyDict({
|
| 31 |
+
'name': 'pl',
|
| 32 |
+
'path': args.dataset,
|
| 33 |
+
'split': args.split
|
| 34 |
+
})
|
| 35 |
+
)
|
| 36 |
+
train_set, test_set = subsets['train'], subsets['test']
|
| 37 |
+
logger.info(f'Successfully load the dataset (size: {len(test_set)})!')
|
| 38 |
+
|
| 39 |
+
# Dock
|
| 40 |
+
logger.info('Start docking...')
|
| 41 |
+
results = []
|
| 42 |
+
for i, data in enumerate(tqdm(test_set)):
|
| 43 |
+
mol = next(iter(Chem.SDMolSupplier(os.path.join(args.ligand_root, data.ligand_filename))))
|
| 44 |
+
# try:
|
| 45 |
+
chem_results = scoring_func.get_chem(mol)
|
| 46 |
+
vina_task = QVinaDockingTask.from_original_data(
|
| 47 |
+
data,
|
| 48 |
+
ligand_root=args.ligand_root,
|
| 49 |
+
protein_root=args.protein_root,
|
| 50 |
+
use_uff=args.use_uff,
|
| 51 |
+
size_factor=args.size_factor
|
| 52 |
+
)
|
| 53 |
+
vina_results = vina_task.run_sync()
|
| 54 |
+
# except:
|
| 55 |
+
# logger.warning('Error #%d' % i)
|
| 56 |
+
# continue
|
| 57 |
+
|
| 58 |
+
results.append({
|
| 59 |
+
'mol': mol,
|
| 60 |
+
'smiles': data.ligand_smiles,
|
| 61 |
+
'ligand_filename': data.ligand_filename,
|
| 62 |
+
'chem_results': chem_results,
|
| 63 |
+
'vina': vina_results
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
# Save
|
| 67 |
+
if args.out is None:
|
| 68 |
+
split_name = os.path.basename(args.split)
|
| 69 |
+
split_name = split_name[:split_name.rfind('.')]
|
| 70 |
+
docked_name = f'{split_name}_test_docked_uff_{args.use_uff}_size_{args.size_factor}.pt'
|
| 71 |
+
out_path = os.path.join(os.path.dirname(args.dataset), docked_name)
|
| 72 |
+
else:
|
| 73 |
+
out_path = args.out
|
| 74 |
+
logger.info('Num docked: %d' % len(results))
|
| 75 |
+
logger.info('Saving results to %s' % out_path)
|
| 76 |
+
torch.save(results, out_path)
|
scripts/evaluate_diffusion.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from rdkit import Chem
|
| 6 |
+
from rdkit import RDLogger
|
| 7 |
+
import torch
|
| 8 |
+
from tqdm.auto import tqdm
|
| 9 |
+
from glob import glob
|
| 10 |
+
from collections import Counter
|
| 11 |
+
|
| 12 |
+
from onescience.utils.targetdiff.evaluation import eval_atom_type, scoring_func, analyze, eval_bond_length
|
| 13 |
+
from onescience.utils.targetdiff import misc, reconstruct, transforms
|
| 14 |
+
from onescience.utils.targetdiff.evaluation.docking_qvina import QVinaDockingTask
|
| 15 |
+
from onescience.utils.targetdiff.evaluation.docking_vina import VinaDockingTask
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def print_dict(d, logger):
|
| 19 |
+
for k, v in d.items():
|
| 20 |
+
if v is not None:
|
| 21 |
+
logger.info(f'{k}:\t{v:.4f}')
|
| 22 |
+
else:
|
| 23 |
+
logger.info(f'{k}:\tNone')
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def print_ring_ratio(all_ring_sizes, logger):
|
| 27 |
+
for ring_size in range(3, 10):
|
| 28 |
+
n_mol = 0
|
| 29 |
+
for counter in all_ring_sizes:
|
| 30 |
+
if ring_size in counter:
|
| 31 |
+
n_mol += 1
|
| 32 |
+
logger.info(f'ring size: {ring_size} ratio: {n_mol / len(all_ring_sizes):.3f}')
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
if __name__ == '__main__':
|
| 36 |
+
parser = argparse.ArgumentParser()
|
| 37 |
+
parser.add_argument('sample_path', type=str)
|
| 38 |
+
parser.add_argument('--verbose', type=eval, default=False)
|
| 39 |
+
parser.add_argument('--eval_step', type=int, default=-1)
|
| 40 |
+
parser.add_argument('--eval_num_examples', type=int, default=None)
|
| 41 |
+
parser.add_argument('--save', type=eval, default=True)
|
| 42 |
+
parser.add_argument('--protein_root', type=str, default='./data/crossdocked_v1.1_rmsd1.0')
|
| 43 |
+
parser.add_argument('--atom_enc_mode', type=str, default='add_aromatic')
|
| 44 |
+
parser.add_argument('--docking_mode', type=str, choices=['qvina', 'vina_score', 'vina_dock', 'none'])
|
| 45 |
+
parser.add_argument('--exhaustiveness', type=int, default=16)
|
| 46 |
+
args = parser.parse_args()
|
| 47 |
+
|
| 48 |
+
result_path = os.path.join(args.sample_path, 'eval_results')
|
| 49 |
+
os.makedirs(result_path, exist_ok=True)
|
| 50 |
+
logger = misc.get_logger('evaluate', log_dir=result_path)
|
| 51 |
+
if not args.verbose:
|
| 52 |
+
RDLogger.DisableLog('rdApp.*')
|
| 53 |
+
|
| 54 |
+
# Load generated data
|
| 55 |
+
results_fn_list = glob(os.path.join(args.sample_path, '*result_*.pt'))
|
| 56 |
+
results_fn_list = sorted(results_fn_list, key=lambda x: int(os.path.basename(x)[:-3].split('_')[-1]))
|
| 57 |
+
if args.eval_num_examples is not None:
|
| 58 |
+
results_fn_list = results_fn_list[:args.eval_num_examples]
|
| 59 |
+
num_examples = len(results_fn_list)
|
| 60 |
+
logger.info(f'Load generated data done! {num_examples} examples in total.')
|
| 61 |
+
|
| 62 |
+
num_samples = 0
|
| 63 |
+
all_mol_stable, all_atom_stable, all_n_atom = 0, 0, 0
|
| 64 |
+
n_recon_success, n_eval_success, n_complete = 0, 0, 0
|
| 65 |
+
results = []
|
| 66 |
+
all_pair_dist, all_bond_dist = [], []
|
| 67 |
+
all_atom_types = Counter()
|
| 68 |
+
success_pair_dist, success_atom_types = [], Counter()
|
| 69 |
+
for example_idx, r_name in enumerate(tqdm(results_fn_list, desc='Eval')):
|
| 70 |
+
r = torch.load(r_name) # ['data', 'pred_ligand_pos', 'pred_ligand_v', 'pred_ligand_pos_traj', 'pred_ligand_v_traj']
|
| 71 |
+
all_pred_ligand_pos = r['pred_ligand_pos_traj'] # [num_samples, num_steps, num_atoms, 3]
|
| 72 |
+
all_pred_ligand_v = r['pred_ligand_v_traj']
|
| 73 |
+
num_samples += len(all_pred_ligand_pos)
|
| 74 |
+
|
| 75 |
+
for sample_idx, (pred_pos, pred_v) in enumerate(zip(all_pred_ligand_pos, all_pred_ligand_v)):
|
| 76 |
+
pred_pos, pred_v = pred_pos[args.eval_step], pred_v[args.eval_step]
|
| 77 |
+
|
| 78 |
+
# stability check
|
| 79 |
+
pred_atom_type = transforms.get_atomic_number_from_index(pred_v, mode=args.atom_enc_mode)
|
| 80 |
+
all_atom_types += Counter(pred_atom_type)
|
| 81 |
+
r_stable = analyze.check_stability(pred_pos, pred_atom_type)
|
| 82 |
+
all_mol_stable += r_stable[0]
|
| 83 |
+
all_atom_stable += r_stable[1]
|
| 84 |
+
all_n_atom += r_stable[2]
|
| 85 |
+
|
| 86 |
+
pair_dist = eval_bond_length.pair_distance_from_pos_v(pred_pos, pred_atom_type)
|
| 87 |
+
all_pair_dist += pair_dist
|
| 88 |
+
|
| 89 |
+
# reconstruction
|
| 90 |
+
try:
|
| 91 |
+
pred_aromatic = transforms.is_aromatic_from_index(pred_v, mode=args.atom_enc_mode)
|
| 92 |
+
mol = reconstruct.reconstruct_from_generated(pred_pos, pred_atom_type, pred_aromatic)
|
| 93 |
+
smiles = Chem.MolToSmiles(mol)
|
| 94 |
+
except reconstruct.MolReconsError:
|
| 95 |
+
if args.verbose:
|
| 96 |
+
logger.warning('Reconstruct failed %s' % f'{example_idx}_{sample_idx}')
|
| 97 |
+
continue
|
| 98 |
+
n_recon_success += 1
|
| 99 |
+
|
| 100 |
+
if '.' in smiles:
|
| 101 |
+
continue
|
| 102 |
+
n_complete += 1
|
| 103 |
+
|
| 104 |
+
# chemical and docking check
|
| 105 |
+
try:
|
| 106 |
+
chem_results = scoring_func.get_chem(mol)
|
| 107 |
+
if args.docking_mode == 'qvina':
|
| 108 |
+
vina_task = QVinaDockingTask.from_generated_mol(
|
| 109 |
+
mol, r['data'].ligand_filename, protein_root=args.protein_root)
|
| 110 |
+
vina_results = vina_task.run_sync()
|
| 111 |
+
elif args.docking_mode in ['vina_score', 'vina_dock']:
|
| 112 |
+
vina_task = VinaDockingTask.from_generated_mol(
|
| 113 |
+
mol, r['data'].ligand_filename, protein_root=args.protein_root)
|
| 114 |
+
score_only_results = vina_task.run(mode='score_only', exhaustiveness=args.exhaustiveness)
|
| 115 |
+
minimize_results = vina_task.run(mode='minimize', exhaustiveness=args.exhaustiveness)
|
| 116 |
+
vina_results = {
|
| 117 |
+
'score_only': score_only_results,
|
| 118 |
+
'minimize': minimize_results
|
| 119 |
+
}
|
| 120 |
+
if args.docking_mode == 'vina_dock':
|
| 121 |
+
docking_results = vina_task.run(mode='dock', exhaustiveness=args.exhaustiveness)
|
| 122 |
+
vina_results['dock'] = docking_results
|
| 123 |
+
else:
|
| 124 |
+
vina_results = None
|
| 125 |
+
|
| 126 |
+
n_eval_success += 1
|
| 127 |
+
except:
|
| 128 |
+
if args.verbose:
|
| 129 |
+
logger.warning('Evaluation failed for %s' % f'{example_idx}_{sample_idx}')
|
| 130 |
+
continue
|
| 131 |
+
|
| 132 |
+
# now we only consider complete molecules as success
|
| 133 |
+
bond_dist = eval_bond_length.bond_distance_from_mol(mol)
|
| 134 |
+
all_bond_dist += bond_dist
|
| 135 |
+
|
| 136 |
+
success_pair_dist += pair_dist
|
| 137 |
+
success_atom_types += Counter(pred_atom_type)
|
| 138 |
+
|
| 139 |
+
results.append({
|
| 140 |
+
'mol': mol,
|
| 141 |
+
'smiles': smiles,
|
| 142 |
+
'ligand_filename': r['data'].ligand_filename,
|
| 143 |
+
'pred_pos': pred_pos,
|
| 144 |
+
'pred_v': pred_v,
|
| 145 |
+
'chem_results': chem_results,
|
| 146 |
+
'vina': vina_results
|
| 147 |
+
})
|
| 148 |
+
logger.info(f'Evaluate done! {num_samples} samples in total.')
|
| 149 |
+
|
| 150 |
+
fraction_mol_stable = all_mol_stable / num_samples
|
| 151 |
+
fraction_atm_stable = all_atom_stable / all_n_atom
|
| 152 |
+
fraction_recon = n_recon_success / num_samples
|
| 153 |
+
fraction_eval = n_eval_success / num_samples
|
| 154 |
+
fraction_complete = n_complete / num_samples
|
| 155 |
+
validity_dict = {
|
| 156 |
+
'mol_stable': fraction_mol_stable,
|
| 157 |
+
'atm_stable': fraction_atm_stable,
|
| 158 |
+
'recon_success': fraction_recon,
|
| 159 |
+
'eval_success': fraction_eval,
|
| 160 |
+
'complete': fraction_complete
|
| 161 |
+
}
|
| 162 |
+
print_dict(validity_dict, logger)
|
| 163 |
+
|
| 164 |
+
c_bond_length_profile = eval_bond_length.get_bond_length_profile(all_bond_dist)
|
| 165 |
+
c_bond_length_dict = eval_bond_length.eval_bond_length_profile(c_bond_length_profile)
|
| 166 |
+
logger.info('JS bond distances of complete mols: ')
|
| 167 |
+
print_dict(c_bond_length_dict, logger)
|
| 168 |
+
|
| 169 |
+
success_pair_length_profile = eval_bond_length.get_pair_length_profile(success_pair_dist)
|
| 170 |
+
success_js_metrics = eval_bond_length.eval_pair_length_profile(success_pair_length_profile)
|
| 171 |
+
print_dict(success_js_metrics, logger)
|
| 172 |
+
|
| 173 |
+
atom_type_js = eval_atom_type.eval_atom_type_distribution(success_atom_types)
|
| 174 |
+
logger.info('Atom type JS: %.4f' % atom_type_js)
|
| 175 |
+
|
| 176 |
+
if args.save:
|
| 177 |
+
eval_bond_length.plot_distance_hist(success_pair_length_profile,
|
| 178 |
+
metrics=success_js_metrics,
|
| 179 |
+
save_path=os.path.join(result_path, f'pair_dist_hist_{args.eval_step}.png'))
|
| 180 |
+
|
| 181 |
+
logger.info('Number of reconstructed mols: %d, complete mols: %d, evaluated mols: %d' % (
|
| 182 |
+
n_recon_success, n_complete, len(results)))
|
| 183 |
+
|
| 184 |
+
qed = [r['chem_results']['qed'] for r in results]
|
| 185 |
+
sa = [r['chem_results']['sa'] for r in results]
|
| 186 |
+
logger.info('QED: Mean: %.3f Median: %.3f' % (np.mean(qed), np.median(qed)))
|
| 187 |
+
logger.info('SA: Mean: %.3f Median: %.3f' % (np.mean(sa), np.median(sa)))
|
| 188 |
+
if args.docking_mode == 'qvina':
|
| 189 |
+
vina = [r['vina'][0]['affinity'] for r in results]
|
| 190 |
+
logger.info('Vina: Mean: %.3f Median: %.3f' % (np.mean(vina), np.median(vina)))
|
| 191 |
+
elif args.docking_mode in ['vina_dock', 'vina_score']:
|
| 192 |
+
vina_score_only = [r['vina']['score_only'][0]['affinity'] for r in results]
|
| 193 |
+
vina_min = [r['vina']['minimize'][0]['affinity'] for r in results]
|
| 194 |
+
logger.info('Vina Score: Mean: %.3f Median: %.3f' % (np.mean(vina_score_only), np.median(vina_score_only)))
|
| 195 |
+
logger.info('Vina Min : Mean: %.3f Median: %.3f' % (np.mean(vina_min), np.median(vina_min)))
|
| 196 |
+
if args.docking_mode == 'vina_dock':
|
| 197 |
+
vina_dock = [r['vina']['dock'][0]['affinity'] for r in results]
|
| 198 |
+
logger.info('Vina Dock : Mean: %.3f Median: %.3f' % (np.mean(vina_dock), np.median(vina_dock)))
|
| 199 |
+
|
| 200 |
+
# check ring distribution
|
| 201 |
+
print_ring_ratio([r['chem_results']['ring_size'] for r in results], logger)
|
| 202 |
+
|
| 203 |
+
if args.save:
|
| 204 |
+
torch.save({
|
| 205 |
+
'stability': validity_dict,
|
| 206 |
+
'bond_length': all_bond_dist,
|
| 207 |
+
'all_results': results
|
| 208 |
+
}, os.path.join(result_path, f'metrics_{args.eval_step}.pt'))
|
scripts/evaluate_from_meta.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
from rdkit import RDLogger
|
| 6 |
+
import torch
|
| 7 |
+
from tqdm.auto import tqdm
|
| 8 |
+
from copy import deepcopy
|
| 9 |
+
|
| 10 |
+
from onescience.utils.targetdiff import misc
|
| 11 |
+
from onescience.utils.targetdiff.evaluation import scoring_func
|
| 12 |
+
from onescience.utils.targetdiff.evaluation.docking_qvina import QVinaDockingTask
|
| 13 |
+
from onescience.utils.targetdiff.evaluation.docking_vina import VinaDockingTask
|
| 14 |
+
from multiprocessing import Pool
|
| 15 |
+
from functools import partial
|
| 16 |
+
from glob import glob
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def eval_single_datapoint(index, id, args):
|
| 20 |
+
if isinstance(index, dict):
|
| 21 |
+
# reference set
|
| 22 |
+
index = [index]
|
| 23 |
+
|
| 24 |
+
ligand_filename = index[0]['ligand_filename']
|
| 25 |
+
num_samples = len(index[:100])
|
| 26 |
+
results = []
|
| 27 |
+
n_eval_success = 0
|
| 28 |
+
for sample_idx, sample_dict in enumerate(tqdm(index[:num_samples], desc='Eval', total=num_samples)):
|
| 29 |
+
mol = sample_dict['mol']
|
| 30 |
+
smiles = sample_dict['smiles']
|
| 31 |
+
if '.' in smiles:
|
| 32 |
+
continue
|
| 33 |
+
# chemical and docking check
|
| 34 |
+
try:
|
| 35 |
+
chem_results = scoring_func.get_chem(mol)
|
| 36 |
+
if 'vina' in sample_dict:
|
| 37 |
+
vina_results = sample_dict['vina']
|
| 38 |
+
else:
|
| 39 |
+
if args.docking_mode == 'qvina':
|
| 40 |
+
vina_task = QVinaDockingTask.from_generated_mol(mol, ligand_filename, protein_root=args.protein_root)
|
| 41 |
+
vina_results = vina_task.run_sync()
|
| 42 |
+
elif args.docking_mode == 'vina':
|
| 43 |
+
vina_task = VinaDockingTask.from_generated_mol(mol, ligand_filename, protein_root=args.protein_root)
|
| 44 |
+
vina_results = vina_task.run(mode='dock')
|
| 45 |
+
elif args.docking_mode in ['vina_full', 'vina_score']:
|
| 46 |
+
vina_task = VinaDockingTask.from_generated_mol(deepcopy(mol),
|
| 47 |
+
ligand_filename, protein_root=args.protein_root)
|
| 48 |
+
score_only_results = vina_task.run(mode='score_only', exhaustiveness=args.exhaustiveness)
|
| 49 |
+
minimize_results = vina_task.run(mode='minimize', exhaustiveness=args.exhaustiveness)
|
| 50 |
+
vina_results = {
|
| 51 |
+
'score_only': score_only_results,
|
| 52 |
+
'minimize': minimize_results
|
| 53 |
+
}
|
| 54 |
+
if args.docking_mode == 'vina_full':
|
| 55 |
+
dock_results = vina_task.run(mode='dock', exhaustiveness=args.exhaustiveness)
|
| 56 |
+
vina_results.update({
|
| 57 |
+
'dock': dock_results,
|
| 58 |
+
})
|
| 59 |
+
|
| 60 |
+
elif args.docking_mode == 'none':
|
| 61 |
+
vina_results = None
|
| 62 |
+
else:
|
| 63 |
+
raise NotImplementedError
|
| 64 |
+
n_eval_success += 1
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.warning('Evaluation failed for %s' % f'{sample_idx}')
|
| 67 |
+
print(str(e))
|
| 68 |
+
continue
|
| 69 |
+
|
| 70 |
+
results.append({
|
| 71 |
+
**sample_dict,
|
| 72 |
+
'chem_results': chem_results,
|
| 73 |
+
'vina': vina_results
|
| 74 |
+
})
|
| 75 |
+
logger.info(f'Evaluate No {id} done! {num_samples} samples in total. {n_eval_success} eval success!')
|
| 76 |
+
torch.save(results, os.path.join(args.result_path, f'eval_{id:03d}_{os.path.basename(ligand_filename[:-4])}.pt'))
|
| 77 |
+
return results
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if __name__ == '__main__':
|
| 81 |
+
parser = argparse.ArgumentParser()
|
| 82 |
+
parser.add_argument('meta_file', type=str) # 'sampling_results/targetdiff_vina_docked.pt'
|
| 83 |
+
parser.add_argument('-n', '--eval_num_examples', type=int, default=100)
|
| 84 |
+
parser.add_argument('--verbose', type=eval, default=False)
|
| 85 |
+
parser.add_argument('--protein_root', type=str, default='./data/crossdocked_v1.1_rmsd1.0')
|
| 86 |
+
parser.add_argument('--docking_mode', type=str, default='vina_full',
|
| 87 |
+
choices=['none', 'qvina', 'vina', 'vina_full', 'vina_score'])
|
| 88 |
+
parser.add_argument('--exhaustiveness', type=int, default=32)
|
| 89 |
+
parser.add_argument('--num_workers', type=int, default=4)
|
| 90 |
+
parser.add_argument('--result_path', type=str, required=True)
|
| 91 |
+
parser.add_argument('--aggregate_meta', type=eval, default=False)
|
| 92 |
+
args = parser.parse_args()
|
| 93 |
+
|
| 94 |
+
if args.result_path:
|
| 95 |
+
os.makedirs(args.result_path, exist_ok=True)
|
| 96 |
+
logger = misc.get_logger('evaluate', args.result_path)
|
| 97 |
+
logger.info(args)
|
| 98 |
+
if not args.verbose:
|
| 99 |
+
RDLogger.DisableLog('rdApp.*')
|
| 100 |
+
|
| 101 |
+
if args.aggregate_meta:
|
| 102 |
+
meta_file_list = sorted(glob(os.path.join(args.meta_file, '*/result.pt')))
|
| 103 |
+
print(f'There are {len(meta_file_list)} files to aggregate')
|
| 104 |
+
test_index = []
|
| 105 |
+
for f in tqdm(meta_file_list, desc='Load meta files'):
|
| 106 |
+
test_index.append(torch.load(f))
|
| 107 |
+
else:
|
| 108 |
+
test_index = torch.load(args.meta_file)
|
| 109 |
+
if isinstance(test_index[0], dict): # single datapoint sampling result
|
| 110 |
+
test_index = [test_index]
|
| 111 |
+
|
| 112 |
+
testset_results = []
|
| 113 |
+
with Pool(args.num_workers) as p:
|
| 114 |
+
for r in tqdm(p.starmap(partial(eval_single_datapoint, args=args),
|
| 115 |
+
zip(test_index[:args.eval_num_examples], list(range(args.eval_num_examples)))),
|
| 116 |
+
total=args.eval_num_examples, desc='Overall Eval'):
|
| 117 |
+
testset_results.append(r)
|
| 118 |
+
|
| 119 |
+
if args.result_path:
|
| 120 |
+
torch.save(testset_results, os.path.join(args.result_path, f'eval_all.pt'))
|
| 121 |
+
|
| 122 |
+
qed = [x['chem_results']['qed'] for r in testset_results for x in r]
|
| 123 |
+
sa = [x['chem_results']['sa'] for r in testset_results for x in r]
|
| 124 |
+
num_atoms = [len(x['pred_pos']) for r in testset_results for x in r]
|
| 125 |
+
logger.info('QED: Mean: %.3f Median: %.3f' % (np.mean(qed), np.median(qed)))
|
| 126 |
+
logger.info('SA: Mean: %.3f Median: %.3f' % (np.mean(sa), np.median(sa)))
|
| 127 |
+
logger.info('Num atoms: Mean: %.3f Median: %.3f' % (np.mean(num_atoms), np.median(num_atoms)))
|
| 128 |
+
if args.docking_mode in ['vina', 'qvina']:
|
| 129 |
+
vina = [x['vina'][0]['affinity'] for r in testset_results for x in r]
|
| 130 |
+
logger.info('Vina: Mean: %.3f Median: %.3f' % (np.mean(vina), np.median(vina)))
|
| 131 |
+
elif args.docking_mode in ['vina_full', 'vina_score']:
|
| 132 |
+
vina_score_only = [x['vina']['score_only'][0]['affinity'] for r in testset_results for x in r]
|
| 133 |
+
vina_min = [x['vina']['minimize'][0]['affinity'] for r in testset_results for x in r]
|
| 134 |
+
logger.info('Vina Score: Mean: %.3f Median: %.3f' % (np.mean(vina_score_only), np.median(vina_score_only)))
|
| 135 |
+
logger.info('Vina Min : Mean: %.3f Median: %.3f' % (np.mean(vina_min), np.median(vina_min)))
|
| 136 |
+
if args.docking_mode == 'vina_full':
|
| 137 |
+
vina_dock = [x['vina']['dock'][0]['affinity'] for r in testset_results for x in r]
|
| 138 |
+
logger.info('Vina Dock : Mean: %.3f Median: %.3f' % (np.mean(vina_dock), np.median(vina_dock)))
|
scripts/inference.sh
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
source ${ROCM_PATH}/cuda/env.sh
|
| 3 |
+
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/lib:$LD_LIBRARY_PATH"
|
| 4 |
+
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/python3.11/site-packages/fastpt/torch/lib:$LD_LIBRARY_PATH"
|
| 5 |
+
|
| 6 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 7 |
+
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
| 8 |
+
#source ${PROJECT_ROOT}/env.sh
|
| 9 |
+
|
| 10 |
+
echo "ONESCIENCE_PATH:" $PROJECT_ROOT
|
| 11 |
+
echo ${ONESCIENCE_DATASETS_DIR}
|
| 12 |
+
echo ${ONESCIENCE_MODELS_DIR}
|
| 13 |
+
|
| 14 |
+
#cd ${PROJECT_ROOT}/examples/biosciences/targetdiff
|
| 15 |
+
|
| 16 |
+
CKPT_PATH=${ONESCIENCE_MODELS_DIR}/targetdiff/pretrained_models/egnn_pdbbind_v2016.pt
|
| 17 |
+
PROTEIN_PATH=${ONESCIENCE_DATASETS_DIR}/targetdiff/examples/3ug2_protein.pdb
|
| 18 |
+
LIGAND_PATH=${ONESCIENCE_DATASETS_DIR}/targetdiff/examples/3ug2_ligand.sdf
|
| 19 |
+
KIND=${4:-Kd}
|
| 20 |
+
DEVICE=${5:-cuda}
|
| 21 |
+
|
| 22 |
+
python scripts/property_prediction/fixed_inference.py \
|
| 23 |
+
--ckpt_path ${CKPT_PATH} \
|
| 24 |
+
--protein_path ${PROTEIN_PATH} \
|
| 25 |
+
--ligand_path ${LIGAND_PATH} \
|
| 26 |
+
--kind ${KIND} \
|
| 27 |
+
--device ${DEVICE}
|
scripts/likelihood_est_diffusion.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import pickle
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from torch_geometric.data import Batch
|
| 8 |
+
from torch_geometric.transforms import Compose
|
| 9 |
+
from tqdm.auto import tqdm
|
| 10 |
+
|
| 11 |
+
import onescience.utils.targetdiff.misc as misc
|
| 12 |
+
import onescience.utils.targetdiff.transforms as trans
|
| 13 |
+
from onescience.datapipes.targetdiff import get_dataset
|
| 14 |
+
from onescience.datapipes.targetdiff.pl_data import FOLLOW_BATCH
|
| 15 |
+
from models.molopt_score_model import ScorePosNet3D
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def data_likelihood_estimation(model, data, time_steps, batch_size=1, device='cuda:0'):
|
| 19 |
+
num_timesteps = len(time_steps)
|
| 20 |
+
num_batch = int(np.ceil(num_timesteps / batch_size))
|
| 21 |
+
all_kl_pos, all_kl_v = [], []
|
| 22 |
+
|
| 23 |
+
cur_i = 0
|
| 24 |
+
# t in [T-1, ..., 0]
|
| 25 |
+
for i in range(num_batch):
|
| 26 |
+
n_data = batch_size if i < num_batch - 1 else num_timesteps - batch_size * (num_batch - 1)
|
| 27 |
+
batch = Batch.from_data_list([data.clone() for _ in range(n_data)], follow_batch=FOLLOW_BATCH).to(device)
|
| 28 |
+
time_step = time_steps[cur_i:cur_i + n_data]
|
| 29 |
+
|
| 30 |
+
kl_pos, kl_v = model.likelihood_estimation(
|
| 31 |
+
protein_pos=batch.protein_pos,
|
| 32 |
+
protein_v=batch.protein_atom_feature.float(),
|
| 33 |
+
batch_protein=batch.protein_element_batch,
|
| 34 |
+
|
| 35 |
+
ligand_pos=batch.ligand_pos,
|
| 36 |
+
ligand_v=batch.ligand_atom_feature_full,
|
| 37 |
+
batch_ligand=batch.ligand_element_batch,
|
| 38 |
+
|
| 39 |
+
time_step=time_step
|
| 40 |
+
)
|
| 41 |
+
all_kl_pos.append(kl_pos)
|
| 42 |
+
all_kl_v.append(kl_v)
|
| 43 |
+
cur_i += n_data
|
| 44 |
+
|
| 45 |
+
# prior
|
| 46 |
+
batch = Batch.from_data_list([data.clone() for _ in range(1)], follow_batch=FOLLOW_BATCH).to(device)
|
| 47 |
+
time_step = torch.tensor([model.num_timesteps], device=device)
|
| 48 |
+
kl_pos_prior, kl_v_prior = model.likelihood_estimation(
|
| 49 |
+
protein_pos=batch.protein_pos,
|
| 50 |
+
protein_v=batch.protein_atom_feature.float(),
|
| 51 |
+
batch_protein=batch.protein_element_batch,
|
| 52 |
+
|
| 53 |
+
ligand_pos=batch.ligand_pos,
|
| 54 |
+
ligand_v=batch.ligand_atom_feature_full,
|
| 55 |
+
batch_ligand=batch.ligand_element_batch,
|
| 56 |
+
|
| 57 |
+
time_step=time_step
|
| 58 |
+
)
|
| 59 |
+
all_kl_pos, all_kl_v = torch.cat(all_kl_pos), torch.cat(all_kl_v)
|
| 60 |
+
sum_kl_pos, sum_kl_v = model.num_timesteps * torch.mean(all_kl_pos), model.num_timesteps * torch.mean(all_kl_v)
|
| 61 |
+
all_kl_pos, all_kl_v = torch.cat([all_kl_pos, kl_pos_prior]), torch.cat([all_kl_v, kl_v_prior])
|
| 62 |
+
sum_kl_pos += kl_pos_prior[0]
|
| 63 |
+
sum_kl_v += kl_v_prior[0]
|
| 64 |
+
return all_kl_pos.cpu(), all_kl_v.cpu(), sum_kl_pos.item(), sum_kl_v.item()
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_dataset_result(dset, affinity_info):
|
| 68 |
+
valid_id = []
|
| 69 |
+
for data_id in tqdm(range(len(dset)), desc='Filtering data'):
|
| 70 |
+
data = dset[data_id]
|
| 71 |
+
ligand_fn_key = data.ligand_filename[:-4]
|
| 72 |
+
pk = affinity_info[ligand_fn_key]['pk']
|
| 73 |
+
if pk > 0:
|
| 74 |
+
valid_id.append(data_id)
|
| 75 |
+
print(f'There are {len(valid_id)} examples with valid pK in total.')
|
| 76 |
+
|
| 77 |
+
all_results = []
|
| 78 |
+
for data_id in tqdm(valid_id, desc='Evaluating'):
|
| 79 |
+
data = dset[data_id]
|
| 80 |
+
# likelihoods
|
| 81 |
+
time_steps = torch.tensor(list(range(0, 1000, 100)), device=args.device)
|
| 82 |
+
all_kl_pos, all_kl_v, sum_kl_pos, sum_kl_v = data_likelihood_estimation(
|
| 83 |
+
model, data, time_steps, batch_size=args.batch_size, device=args.device)
|
| 84 |
+
kl = sum_kl_pos + sum_kl_v
|
| 85 |
+
|
| 86 |
+
# embedding
|
| 87 |
+
batch = Batch.from_data_list([data.clone() for _ in range(1)], follow_batch=FOLLOW_BATCH).to(args.device)
|
| 88 |
+
preds = model.fetch_embedding(
|
| 89 |
+
protein_pos=batch.protein_pos,
|
| 90 |
+
protein_v=batch.protein_atom_feature.float(),
|
| 91 |
+
batch_protein=batch.protein_element_batch,
|
| 92 |
+
|
| 93 |
+
ligand_pos=batch.ligand_pos,
|
| 94 |
+
ligand_v=batch.ligand_atom_feature_full,
|
| 95 |
+
batch_ligand=batch.ligand_element_batch,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# gather results
|
| 99 |
+
ligand_fn_key = data.ligand_filename[:-4]
|
| 100 |
+
result = {
|
| 101 |
+
'idx': data_id,
|
| 102 |
+
**affinity_info[ligand_fn_key],
|
| 103 |
+
'kl_pos': all_kl_pos,
|
| 104 |
+
'kl_v': all_kl_v,
|
| 105 |
+
'nll': kl,
|
| 106 |
+
'pred_ligand_v': preds['pred_ligand_v'].cpu(),
|
| 107 |
+
'final_h': preds['final_h'].cpu(),
|
| 108 |
+
'final_ligand_h': preds['final_ligand_h'].cpu()
|
| 109 |
+
}
|
| 110 |
+
all_results.append(result)
|
| 111 |
+
return all_results
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == '__main__':
|
| 115 |
+
parser = argparse.ArgumentParser()
|
| 116 |
+
parser.add_argument('--device', type=str, default='cuda:0')
|
| 117 |
+
parser.add_argument('--config', type=str, default='configs/sampling/final_diffusion/aromatic_176k.yml')
|
| 118 |
+
parser.add_argument('--affinity_path', type=str, default='data/affinity_info.pkl')
|
| 119 |
+
parser.add_argument('--index_path', type=str, default='data/crossdocked_v1.1_rmsd1.0_pocket10/index.pkl')
|
| 120 |
+
parser.add_argument('--batch_size', type=int, default=4)
|
| 121 |
+
parser.add_argument('--result_path', type=str, default='./outputs_embedding')
|
| 122 |
+
|
| 123 |
+
args = parser.parse_args()
|
| 124 |
+
|
| 125 |
+
logger = misc.get_logger('evaluate')
|
| 126 |
+
|
| 127 |
+
if os.path.exists(args.affinity_path):
|
| 128 |
+
with open(args.affinity_path, 'rb') as f:
|
| 129 |
+
affinity_info = pickle.load(f)
|
| 130 |
+
else:
|
| 131 |
+
# collect index
|
| 132 |
+
with open(args.index_path, 'rb') as f:
|
| 133 |
+
index = pickle.load(f)
|
| 134 |
+
affinity_info = {}
|
| 135 |
+
for pdb_file, sdf_file, rmsd in index:
|
| 136 |
+
affinity_info[sdf_file[:-4]] = {'rmsd': rmsd}
|
| 137 |
+
|
| 138 |
+
# fetch reference vina score / binding affinity
|
| 139 |
+
types_path = 'data/CrossDocked2020/types/it2_tt_v1.1_completeset_train0.types'
|
| 140 |
+
with open(types_path, 'r') as f:
|
| 141 |
+
for ln in tqdm(f.readlines()):
|
| 142 |
+
# <label> <pK> <RMSD to crystal> <Receptor> <Ligand> # <Autodock Vina score>
|
| 143 |
+
_, pk, rmsd, protein_fn, ligand_fn, vina = ln.split()
|
| 144 |
+
ligand_raw_fn = ligand_fn[:ligand_fn.rfind('.')]
|
| 145 |
+
if ligand_raw_fn in affinity_info:
|
| 146 |
+
affinity_info[ligand_raw_fn].update({
|
| 147 |
+
'pk': float(pk),
|
| 148 |
+
'vina': float(vina[1:])
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
# save affinity info
|
| 152 |
+
with open(args.affinity_path, 'wb') as f:
|
| 153 |
+
pickle.dump(affinity_info, f)
|
| 154 |
+
|
| 155 |
+
# Load config
|
| 156 |
+
config = misc.load_config(args.config)
|
| 157 |
+
logger.info(config)
|
| 158 |
+
misc.seed_all(config.sample.seed)
|
| 159 |
+
|
| 160 |
+
# Load checkpoint
|
| 161 |
+
ckpt = torch.load(config.model.checkpoint, map_location=args.device)
|
| 162 |
+
|
| 163 |
+
# Transforms
|
| 164 |
+
protein_featurizer = trans.FeaturizeProteinAtom()
|
| 165 |
+
ligand_featurizer = trans.FeaturizeLigandAtom(ckpt['config'].data.transform.ligand_atom_mode)
|
| 166 |
+
transform_list = [
|
| 167 |
+
protein_featurizer,
|
| 168 |
+
ligand_featurizer,
|
| 169 |
+
trans.FeaturizeLigandBond(),
|
| 170 |
+
]
|
| 171 |
+
if ckpt['config'].data.transform.random_rot:
|
| 172 |
+
transform_list.append(trans.RandomRotation())
|
| 173 |
+
transform = Compose(transform_list)
|
| 174 |
+
|
| 175 |
+
# Load dataset
|
| 176 |
+
dataset, subsets = get_dataset(
|
| 177 |
+
config=ckpt['config'].data,
|
| 178 |
+
transform=transform
|
| 179 |
+
)
|
| 180 |
+
train_set, test_set = subsets['train'], subsets['test']
|
| 181 |
+
logger.info(f'Successfully load the dataset (size: {len(test_set)})!')
|
| 182 |
+
|
| 183 |
+
# Load model
|
| 184 |
+
model = ScorePosNet3D(
|
| 185 |
+
ckpt['config'].model,
|
| 186 |
+
protein_atom_feature_dim=protein_featurizer.feature_dim,
|
| 187 |
+
ligand_atom_feature_dim=ligand_featurizer.feature_dim
|
| 188 |
+
).to(args.device)
|
| 189 |
+
model.load_state_dict(ckpt['model'], strict=False if 'train_config' in config.model else True)
|
| 190 |
+
logger.info(f'Successfully load the model! {config.model.checkpoint}')
|
| 191 |
+
|
| 192 |
+
# filter data with valid pK, compute likelihood and embedding
|
| 193 |
+
valid_train_results = get_dataset_result(train_set, affinity_info)
|
| 194 |
+
valid_test_results = get_dataset_result(test_set, affinity_info)
|
| 195 |
+
|
| 196 |
+
os.makedirs(args.result_path, exist_ok=True)
|
| 197 |
+
torch.save(valid_train_results, os.path.join(args.result_path, 'crossdocked_train.pt'))
|
| 198 |
+
torch.save(valid_test_results, os.path.join(args.result_path, 'crossdocked_test.pt'))
|
scripts/property_prediction/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TargetDiff property prediction scripts."""
|
| 2 |
+
|
scripts/property_prediction/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (272 Bytes). View file
|
|
|
scripts/property_prediction/__pycache__/local_misc_prop.cpython-311.pyc
ADDED
|
Binary file (3.31 kB). View file
|
|
|
scripts/property_prediction/eval_prop.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
from tqdm.auto import tqdm
|
| 3 |
+
import torch
|
| 4 |
+
import torch.utils.tensorboard
|
| 5 |
+
from torch_geometric.transforms import Compose
|
| 6 |
+
|
| 7 |
+
from onescience.datapipes.targetdiff import get_dataset
|
| 8 |
+
import onescience.utils.targetdiff.transforms_prop as utils_trans
|
| 9 |
+
import onescience.utils.targetdiff.misc as utils_misc
|
| 10 |
+
import numpy as np
|
| 11 |
+
from onescience.datapipes.targetdiff.protein_ligand import KMAP
|
| 12 |
+
from scripts.property_prediction.local_misc_prop import get_model, get_dataloader, get_eval_scores
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
parser = argparse.ArgumentParser()
|
| 17 |
+
parser.add_argument('--ckpt_path', type=str)
|
| 18 |
+
parser.add_argument('--device', type=str, default='cuda')
|
| 19 |
+
parser.add_argument('--seed', type=int, default=2021)
|
| 20 |
+
args = parser.parse_args()
|
| 21 |
+
utils_misc.seed_all(args.seed)
|
| 22 |
+
|
| 23 |
+
# Logging
|
| 24 |
+
logger = utils_misc.get_logger('eval')
|
| 25 |
+
logger.info(args)
|
| 26 |
+
|
| 27 |
+
# Load config
|
| 28 |
+
logger.info(f'Loading model from {args.ckpt_path}')
|
| 29 |
+
ckpt_restore = torch.load(args.ckpt_path, map_location=torch.device('cpu'))
|
| 30 |
+
config = ckpt_restore['config']
|
| 31 |
+
logger.info(f'ckpt_config: {config}')
|
| 32 |
+
|
| 33 |
+
# Transforms
|
| 34 |
+
protein_featurizer = utils_trans.FeaturizeProteinAtom()
|
| 35 |
+
ligand_featurizer = utils_trans.FeaturizeLigandAtom()
|
| 36 |
+
transform = Compose([
|
| 37 |
+
protein_featurizer,
|
| 38 |
+
ligand_featurizer,
|
| 39 |
+
])
|
| 40 |
+
|
| 41 |
+
# Load model
|
| 42 |
+
model = get_model(config, protein_featurizer.feature_dim, ligand_featurizer.feature_dim)
|
| 43 |
+
model.load_state_dict(ckpt_restore['model'])
|
| 44 |
+
model = model.to(args.device)
|
| 45 |
+
# print(model)
|
| 46 |
+
# logger.info(f'# trainable parameters: {utils_misc.count_parameters(model) / 1e6:.4f} M')
|
| 47 |
+
model.eval()
|
| 48 |
+
|
| 49 |
+
# Datasets and loaders
|
| 50 |
+
# config.dataset.path = './data/pdbbind_v2020/pocket_10_refined'
|
| 51 |
+
# config.dataset.split = './data/pdbbind_v2020/pocket_10_refined/split.pt'
|
| 52 |
+
logger.info('Loading dataset...')
|
| 53 |
+
dataset, subsets = get_dataset(
|
| 54 |
+
config=config.dataset,
|
| 55 |
+
transform=transform,
|
| 56 |
+
heavy_only=config.dataset.get('heavy_only', False)
|
| 57 |
+
)
|
| 58 |
+
train_set, val_set, test_set = subsets['train'], subsets['val'], subsets['test']
|
| 59 |
+
logger.info(f'Train set: {len(train_set)} Val set: {len(val_set)} Test set: {len(test_set)}')
|
| 60 |
+
train_loader, val_loader, test_loader = get_dataloader(train_set, val_set, test_set, config)
|
| 61 |
+
|
| 62 |
+
def validate(epoch, data_loader, prefix='Test'):
|
| 63 |
+
sum_loss, sum_n = 0, 0
|
| 64 |
+
ytrue_arr, ypred_arr = [], []
|
| 65 |
+
y_kind = []
|
| 66 |
+
with torch.no_grad():
|
| 67 |
+
model.eval()
|
| 68 |
+
for batch in tqdm(data_loader, desc=prefix):
|
| 69 |
+
batch = batch.to(args.device)
|
| 70 |
+
loss, pred = model.get_loss(batch, pos_noise_std=0., return_pred=True)
|
| 71 |
+
sum_loss += loss.item() * len(batch.y)
|
| 72 |
+
sum_n += len(batch.y)
|
| 73 |
+
ypred_arr.append(pred.view(-1))
|
| 74 |
+
ytrue_arr.append(batch.y)
|
| 75 |
+
y_kind.append(batch.kind)
|
| 76 |
+
avg_loss = sum_loss / sum_n
|
| 77 |
+
logger.info('[%s] Epoch %03d | Loss %.6f' % (
|
| 78 |
+
prefix, epoch, avg_loss,
|
| 79 |
+
))
|
| 80 |
+
ypred_arr = torch.cat(ypred_arr).cpu().numpy().astype(np.float64)
|
| 81 |
+
ytrue_arr = torch.cat(ytrue_arr).cpu().numpy().astype(np.float64)
|
| 82 |
+
y_kind = torch.cat(y_kind).cpu().numpy()
|
| 83 |
+
rmse = get_eval_scores(ypred_arr, ytrue_arr, logger)
|
| 84 |
+
for k, v in KMAP.items():
|
| 85 |
+
get_eval_scores(ypred_arr[y_kind == v], ytrue_arr[y_kind == v], logger, prefix=k)
|
| 86 |
+
return avg_loss
|
| 87 |
+
|
| 88 |
+
test_loss = validate(ckpt_restore['epoch'], test_loader)
|
| 89 |
+
print('Test loss: ', test_loss)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if __name__ == '__main__':
|
| 93 |
+
main()
|
scripts/property_prediction/extract_pockets.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import multiprocessing as mp
|
| 3 |
+
import os
|
| 4 |
+
import pickle
|
| 5 |
+
from functools import partial
|
| 6 |
+
|
| 7 |
+
from rdkit import RDLogger
|
| 8 |
+
from tqdm.auto import tqdm
|
| 9 |
+
|
| 10 |
+
from onescience.datapipes.targetdiff.protein_ligand import parse_sdf_file_mol, read_mol, KMAP
|
| 11 |
+
from onescience.utils.targetdiff.data import PDBProtein
|
| 12 |
+
|
| 13 |
+
RDLogger.DisableLog('rdApp.*')
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def parse_pdbbind_index_file(raw_path, subset='refined'):
|
| 17 |
+
all_index = []
|
| 18 |
+
version = int(raw_path.rstrip('/')[-4:])
|
| 19 |
+
assert version >= 2016
|
| 20 |
+
if subset == 'refined':
|
| 21 |
+
data_path = os.path.join(raw_path, f'refined-set')
|
| 22 |
+
index_path = os.path.join(data_path, 'index', f'INDEX_refined_data.{version}')
|
| 23 |
+
elif subset == 'general':
|
| 24 |
+
data_path = os.path.join(raw_path, f'general-set-except-refined')
|
| 25 |
+
index_path = os.path.join(data_path, 'index', f'INDEX_general_PL_data.{version}')
|
| 26 |
+
else:
|
| 27 |
+
raise ValueError(subset)
|
| 28 |
+
|
| 29 |
+
all_files = os.listdir(data_path)
|
| 30 |
+
with open(index_path, 'r') as f:
|
| 31 |
+
lines = f.readlines()
|
| 32 |
+
for line in lines:
|
| 33 |
+
if line.startswith('#'): continue
|
| 34 |
+
index, res, year, pka, kv = line.split('//')[0].strip().split()
|
| 35 |
+
kind = [v for k, v in KMAP.items() if k in kv]
|
| 36 |
+
assert len(kind) == 1
|
| 37 |
+
if index in all_files:
|
| 38 |
+
all_index.append([index, res, year, pka, kind[0]])
|
| 39 |
+
return all_index
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def process_item(item, args):
|
| 43 |
+
pdb_idx, res, year, pka, kind = item
|
| 44 |
+
ligand_path = None
|
| 45 |
+
try:
|
| 46 |
+
if args.subset == 'refined':
|
| 47 |
+
pdb_path = os.path.join(args.source, 'refined-set', pdb_idx)
|
| 48 |
+
elif args.subset == 'general':
|
| 49 |
+
pdb_path = os.path.join(args.source, 'general-set-except-refined', pdb_idx)
|
| 50 |
+
else:
|
| 51 |
+
raise ValueError(args.subset)
|
| 52 |
+
|
| 53 |
+
protein_path = os.path.join(pdb_path, f'{pdb_idx}_protein.pdb')
|
| 54 |
+
ligand_sdf_path = os.path.join(pdb_path, f'{pdb_idx}_ligand.sdf')
|
| 55 |
+
ligand_mol2_path = os.path.join(pdb_path, f'{pdb_idx}_ligand.mol2')
|
| 56 |
+
mol, problem, ligand_path = read_mol(ligand_sdf_path, ligand_mol2_path)
|
| 57 |
+
if problem:
|
| 58 |
+
print('Read mol error.', item)
|
| 59 |
+
return None, ligand_path, res, pka, kind
|
| 60 |
+
|
| 61 |
+
protein = PDBProtein(protein_path)
|
| 62 |
+
# ligand = parse_sdf_file_mol(ligand_path, heavy_only=True)
|
| 63 |
+
ligand = parse_sdf_file_mol(ligand_path, heavy_only=False)
|
| 64 |
+
if args.dest is None:
|
| 65 |
+
pocket_path = os.path.join(pdb_path, f'{pdb_idx}_pocket{args.radius}.pdb')
|
| 66 |
+
else:
|
| 67 |
+
pocket_dir = os.path.join(args.dest, f'pocket_{args.radius}_{args.subset}', 'pockets')
|
| 68 |
+
os.makedirs(pocket_dir, exist_ok=True)
|
| 69 |
+
pocket_path = os.path.join(pocket_dir, f'{pdb_idx}_pocket{args.radius}.pdb')
|
| 70 |
+
if not os.path.exists(pocket_path):
|
| 71 |
+
pdb_block_pocket = protein.residues_to_pdb_block(
|
| 72 |
+
protein.query_residues_ligand(ligand, args.radius)
|
| 73 |
+
)
|
| 74 |
+
with open(pocket_path, 'w') as f:
|
| 75 |
+
f.write(pdb_block_pocket)
|
| 76 |
+
return pocket_path, ligand_path, res, pka, kind
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print('Exception occured.', item, repr(e))
|
| 80 |
+
return None, ligand_path, res, pka, kind
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == '__main__':
|
| 84 |
+
parser = argparse.ArgumentParser()
|
| 85 |
+
parser.add_argument('--source', type=str, default='./data/pdbbind_v2016')
|
| 86 |
+
parser.add_argument('--dest', type=str, default=None)
|
| 87 |
+
parser.add_argument('--fixed_sdf_dir', type=str, default='./data/pdbbind_v2016/fixed_sdf_files')
|
| 88 |
+
parser.add_argument('--subset', type=str, default='refined')
|
| 89 |
+
parser.add_argument('--refined_index_pkl', type=str, default=None)
|
| 90 |
+
parser.add_argument('--radius', type=int, default=10)
|
| 91 |
+
parser.add_argument('--num_workers', type=int, default=16)
|
| 92 |
+
args = parser.parse_args()
|
| 93 |
+
|
| 94 |
+
index = parse_pdbbind_index_file(args.source, args.subset)
|
| 95 |
+
# if not os.path.exists(args.fixed_sdf_dir):
|
| 96 |
+
# os.makedirs(args.fixed_sdf_dir)
|
| 97 |
+
|
| 98 |
+
pool = mp.Pool(args.num_workers)
|
| 99 |
+
index_pocket = []
|
| 100 |
+
for item_pocket in tqdm(pool.imap_unordered(partial(process_item, args=args), index), total=len(index)):
|
| 101 |
+
index_pocket.append(item_pocket)
|
| 102 |
+
pool.close()
|
| 103 |
+
|
| 104 |
+
valid_index_pocket = []
|
| 105 |
+
for index in index_pocket:
|
| 106 |
+
if index[0] is not None:
|
| 107 |
+
valid_index_pocket.append(index)
|
| 108 |
+
|
| 109 |
+
output_root = args.source if args.dest is None else args.dest
|
| 110 |
+
save_path = os.path.join(output_root, f'pocket_{args.radius}_{args.subset}')
|
| 111 |
+
if not os.path.exists(save_path):
|
| 112 |
+
os.makedirs(save_path)
|
| 113 |
+
index_path = os.path.join(save_path, 'index.pkl')
|
| 114 |
+
|
| 115 |
+
if args.subset == 'general' and args.refined_index_pkl is not None:
|
| 116 |
+
with open(args.refined_index_pkl, 'rb') as f:
|
| 117 |
+
refined_index = pickle.load(f)
|
| 118 |
+
valid_index_pocket += refined_index
|
| 119 |
+
with open(index_path, 'wb') as f:
|
| 120 |
+
pickle.dump(valid_index_pocket, f)
|
| 121 |
+
print('Done. %d protein-ligand pairs in total.' % len(valid_index_pocket))
|
scripts/property_prediction/fixed_inference.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.utils.tensorboard
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
from torch_geometric.loader import DataLoader
|
| 7 |
+
from tqdm.auto import tqdm
|
| 8 |
+
from torch_geometric.transforms import Compose
|
| 9 |
+
|
| 10 |
+
import onescience.utils.targetdiff.misc as utils_misc
|
| 11 |
+
from onescience.datapipes.targetdiff.protein_ligand import KMAP, parse_sdf_file_mol
|
| 12 |
+
from onescience.datapipes.targetdiff.pl_data import ProteinLigandData, torchify_dict
|
| 13 |
+
from onescience.utils.targetdiff.data import PDBProtein
|
| 14 |
+
import onescience.utils.targetdiff.transforms_prop as utils_trans
|
| 15 |
+
from scripts.property_prediction.local_misc_prop import get_model
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class InferenceDataset(Dataset):
|
| 19 |
+
def __init__(self, data_list):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.data_list = data_list
|
| 22 |
+
|
| 23 |
+
def __len__(self):
|
| 24 |
+
return len(self.data_list)
|
| 25 |
+
|
| 26 |
+
def __getitem__(self, idx):
|
| 27 |
+
data = self.data_list[idx]
|
| 28 |
+
return data
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def convert_data(pdb_path, ligand_path, transform, radius=10, pocket=False, heavy_only=False):
|
| 32 |
+
ligand_dict = parse_sdf_file_mol(ligand_path, heavy_only=heavy_only)
|
| 33 |
+
if not pocket:
|
| 34 |
+
protein = PDBProtein(pdb_path)
|
| 35 |
+
pdb_block_pocket = protein.residues_to_pdb_block(
|
| 36 |
+
protein.query_residues_ligand(ligand_dict, radius)
|
| 37 |
+
)
|
| 38 |
+
pocket_dict = PDBProtein(pdb_block_pocket).to_dict_atom()
|
| 39 |
+
else:
|
| 40 |
+
pocket_dict = PDBProtein(pdb_path).to_dict_atom()
|
| 41 |
+
|
| 42 |
+
data = ProteinLigandData.from_protein_ligand_dicts(
|
| 43 |
+
protein_dict=torchify_dict(pocket_dict),
|
| 44 |
+
ligand_dict=torchify_dict(ligand_dict),
|
| 45 |
+
)
|
| 46 |
+
data.protein_filename = pdb_path
|
| 47 |
+
data.ligand_filename = ligand_path
|
| 48 |
+
assert data.protein_pos.size(0) > 0
|
| 49 |
+
if transform is not None:
|
| 50 |
+
data = transform(data)
|
| 51 |
+
return data
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def adjust_ligand_features_for_compatibility(batch, expected_ligand_dim=30):
|
| 55 |
+
"""
|
| 56 |
+
调整ligand特征以匹配模型期望的维度
|
| 57 |
+
对于RDKit版本差异导致的特征维度不匹配问题
|
| 58 |
+
"""
|
| 59 |
+
if batch.ligand_atom_feature_full.size(-1) == expected_ligand_dim:
|
| 60 |
+
# 特征维度已经匹配,不需要调整
|
| 61 |
+
return batch
|
| 62 |
+
|
| 63 |
+
current_dim = batch.ligand_atom_feature_full.size(-1)
|
| 64 |
+
print(f"Ligand features dimension mismatch: got {current_dim}, expected {expected_ligand_dim}")
|
| 65 |
+
|
| 66 |
+
if current_dim > expected_ligand_dim:
|
| 67 |
+
# 当前特征维度更大,截取前面的部分
|
| 68 |
+
print(f"Truncating ligand features from {current_dim} to {expected_ligand_dim}")
|
| 69 |
+
batch.ligand_atom_feature_full = batch.ligand_atom_feature_full[:, :expected_ligand_dim]
|
| 70 |
+
elif current_dim < expected_ligand_dim:
|
| 71 |
+
# 当前特征维度较小,用零填充
|
| 72 |
+
print(f"Padding ligand features from {current_dim} to {expected_ligand_dim}")
|
| 73 |
+
padding = torch.zeros(batch.ligand_atom_feature_full.size(0),
|
| 74 |
+
expected_ligand_dim - current_dim,
|
| 75 |
+
dtype=batch.ligand_atom_feature_full.dtype,
|
| 76 |
+
device=batch.ligand_atom_feature_full.device)
|
| 77 |
+
batch.ligand_atom_feature_full = torch.cat([batch.ligand_atom_feature_full, padding], dim=-1)
|
| 78 |
+
|
| 79 |
+
return batch
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def load_model_compatible(ckpt_restore, protein_featurizer, ligand_featurizer):
|
| 83 |
+
"""
|
| 84 |
+
智能加载模型,兼容不同版本的RDKit导致的参数形状差异
|
| 85 |
+
"""
|
| 86 |
+
config = ckpt_restore['config']
|
| 87 |
+
|
| 88 |
+
# 首先尝试按原始方式加载模型
|
| 89 |
+
try:
|
| 90 |
+
model = get_model(config, protein_featurizer.feature_dim, ligand_featurizer.feature_dim)
|
| 91 |
+
model.load_state_dict(ckpt_restore['model'])
|
| 92 |
+
return model
|
| 93 |
+
except RuntimeError as e:
|
| 94 |
+
if "size mismatch" in str(e) and "ligand_atom_emb.weight" in str(e):
|
| 95 |
+
print(f"Found size mismatch error: {e}")
|
| 96 |
+
|
| 97 |
+
# 从checkpoint中推断原始的ligand特征维度
|
| 98 |
+
original_ligand_feature_dim = None
|
| 99 |
+
for key, tensor in ckpt_restore['model'].items():
|
| 100 |
+
if 'ligand_atom_emb.weight' in key:
|
| 101 |
+
original_ligand_feature_dim = tensor.shape[1]
|
| 102 |
+
break
|
| 103 |
+
|
| 104 |
+
if original_ligand_feature_dim is not None:
|
| 105 |
+
print(f"Detected original ligand feature dimension: {original_ligand_feature_dim}")
|
| 106 |
+
|
| 107 |
+
# 创建具有正确维度的模型
|
| 108 |
+
# 注意:这里我们传入原始的ligand featurizer维度,但稍后会调整输入数据
|
| 109 |
+
model = get_model(config, protein_featurizer.feature_dim, original_ligand_feature_dim)
|
| 110 |
+
|
| 111 |
+
# 智能加载状态字典,只加载形状匹配的参数
|
| 112 |
+
model_dict = model.state_dict()
|
| 113 |
+
pretrained_dict = {}
|
| 114 |
+
skipped_params = []
|
| 115 |
+
|
| 116 |
+
for k, v in ckpt_restore['model'].items():
|
| 117 |
+
if k in model_dict:
|
| 118 |
+
if model_dict[k].shape == v.shape:
|
| 119 |
+
pretrained_dict[k] = v
|
| 120 |
+
else:
|
| 121 |
+
print(f"Shape mismatch for {k}: checkpoint {v.shape} vs model {model_dict[k].shape}")
|
| 122 |
+
skipped_params.append(k)
|
| 123 |
+
else:
|
| 124 |
+
print(f"Parameter {k} not found in current model")
|
| 125 |
+
skipped_params.append(k)
|
| 126 |
+
|
| 127 |
+
print(f"Loaded {len(pretrained_dict)} parameters, skipped {len(skipped_params)} parameters")
|
| 128 |
+
|
| 129 |
+
# 更新模型状态字典
|
| 130 |
+
model_dict.update(pretrained_dict)
|
| 131 |
+
model.load_state_dict(model_dict)
|
| 132 |
+
|
| 133 |
+
print("Model loaded successfully with compatibility adjustments")
|
| 134 |
+
return model
|
| 135 |
+
else:
|
| 136 |
+
raise e
|
| 137 |
+
else:
|
| 138 |
+
raise e
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def main():
|
| 142 |
+
parser = argparse.ArgumentParser()
|
| 143 |
+
parser.add_argument('--ckpt_path', type=str)
|
| 144 |
+
parser.add_argument('--protein_path', type=str)
|
| 145 |
+
parser.add_argument('--ligand_path', type=str)
|
| 146 |
+
parser.add_argument('--kind', type=str, default='Ki', choices=['Ki', 'Kd', 'IC50'])
|
| 147 |
+
parser.add_argument('--device', type=str, default='cuda')
|
| 148 |
+
parser.add_argument('--seed', type=int, default=2021)
|
| 149 |
+
args = parser.parse_args()
|
| 150 |
+
utils_misc.seed_all(args.seed)
|
| 151 |
+
|
| 152 |
+
# Logging
|
| 153 |
+
logger = utils_misc.get_logger('eval')
|
| 154 |
+
logger.info(args)
|
| 155 |
+
|
| 156 |
+
# Load config
|
| 157 |
+
logger.info(f'Loading model from {args.ckpt_path}')
|
| 158 |
+
ckpt_restore = torch.load(args.ckpt_path, map_location=torch.device('cpu'), weights_only=False)
|
| 159 |
+
config = ckpt_restore['config']
|
| 160 |
+
logger.info(f'ckpt_config: {config}')
|
| 161 |
+
|
| 162 |
+
# Transforms
|
| 163 |
+
protein_featurizer = utils_trans.FeaturizeProteinAtom()
|
| 164 |
+
ligand_featurizer = utils_trans.FeaturizeLigandAtom()
|
| 165 |
+
transform = Compose([
|
| 166 |
+
protein_featurizer,
|
| 167 |
+
ligand_featurizer,
|
| 168 |
+
])
|
| 169 |
+
|
| 170 |
+
# Load model with compatibility fix
|
| 171 |
+
model = load_model_compatible(ckpt_restore, protein_featurizer, ligand_featurizer)
|
| 172 |
+
|
| 173 |
+
# Get the expected ligand feature dimension from the loaded model
|
| 174 |
+
expected_ligand_dim = None
|
| 175 |
+
for name, param in model.named_parameters():
|
| 176 |
+
if 'ligand_atom_emb.weight' in name:
|
| 177 |
+
expected_ligand_dim = param.shape[1] # input dimension
|
| 178 |
+
break
|
| 179 |
+
|
| 180 |
+
if expected_ligand_dim is None:
|
| 181 |
+
# If we can't find the dimension from parameters, it means we couldn't load that layer
|
| 182 |
+
# Use the value detected during model loading
|
| 183 |
+
expected_ligand_dim = 30 # Default based on error message
|
| 184 |
+
|
| 185 |
+
model = model.to(args.device)
|
| 186 |
+
logger.info(f'# trainable parameters: {utils_misc.count_parameters(model) / 1e6:.4f} M')
|
| 187 |
+
model.eval()
|
| 188 |
+
|
| 189 |
+
test_data = convert_data(args.protein_path, args.ligand_path, transform,
|
| 190 |
+
heavy_only=config.dataset.get('heavy_only', False))
|
| 191 |
+
test_data.kind = KMAP[args.kind]
|
| 192 |
+
test_set = InferenceDataset([test_data])
|
| 193 |
+
test_loader = DataLoader(test_set, batch_size=1, shuffle=False,
|
| 194 |
+
follow_batch=['protein_element', 'ligand_element'])
|
| 195 |
+
|
| 196 |
+
with torch.no_grad():
|
| 197 |
+
model.eval()
|
| 198 |
+
for batch in tqdm(test_loader, desc='Inference'):
|
| 199 |
+
batch = batch.to(args.device)
|
| 200 |
+
|
| 201 |
+
# Adjust ligand features to match the expected dimension
|
| 202 |
+
batch = adjust_ligand_features_for_compatibility(batch, expected_ligand_dim)
|
| 203 |
+
|
| 204 |
+
pred = model(
|
| 205 |
+
protein_pos=batch.protein_pos,
|
| 206 |
+
protein_atom_feature=batch.protein_atom_feature.float(),
|
| 207 |
+
ligand_pos=batch.ligand_pos,
|
| 208 |
+
ligand_atom_feature=batch.ligand_atom_feature_full.float(),
|
| 209 |
+
batch_protein=batch.protein_element_batch,
|
| 210 |
+
batch_ligand=batch.ligand_element_batch,
|
| 211 |
+
output_kind=batch.kind
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
print(f'PDB ID: {batch.protein_filename[0]} '
|
| 215 |
+
f'Prediction: {args.kind}={unit_transform(pred.cpu().squeeze()):.2e} m')
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def unit_transform(pka):
|
| 219 |
+
# pka = -log10 Kd / Ki
|
| 220 |
+
affinity = torch.pow(10, -pka.cpu().squeeze())
|
| 221 |
+
return affinity
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
if __name__ == '__main__':
|
| 225 |
+
main()
|
scripts/property_prediction/inference.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.utils.tensorboard
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
from torch_geometric.loader import DataLoader
|
| 7 |
+
from tqdm.auto import tqdm
|
| 8 |
+
from torch_geometric.transforms import Compose
|
| 9 |
+
|
| 10 |
+
import onescience.utils.targetdiff.misc as utils_misc
|
| 11 |
+
from onescience.datapipes.targetdiff.protein_ligand import KMAP, parse_sdf_file_mol
|
| 12 |
+
from onescience.datapipes.targetdiff.pl_data import ProteinLigandData, torchify_dict
|
| 13 |
+
from onescience.utils.targetdiff.data import PDBProtein
|
| 14 |
+
import onescience.utils.targetdiff.transforms_prop as utils_trans
|
| 15 |
+
from scripts.property_prediction.local_misc_prop import get_model
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class InferenceDataset(Dataset):
|
| 19 |
+
def __init__(self, data_list):
|
| 20 |
+
super().__init__()
|
| 21 |
+
self.data_list = data_list
|
| 22 |
+
|
| 23 |
+
def __len__(self):
|
| 24 |
+
return len(self.data_list)
|
| 25 |
+
|
| 26 |
+
def __getitem__(self, idx):
|
| 27 |
+
data = self.data_list[idx]
|
| 28 |
+
return data
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def convert_data(pdb_path, ligand_path, transform, radius=10, pocket=False, heavy_only=False):
|
| 32 |
+
ligand_dict = parse_sdf_file_mol(ligand_path, heavy_only=heavy_only)
|
| 33 |
+
if not pocket:
|
| 34 |
+
protein = PDBProtein(pdb_path)
|
| 35 |
+
pdb_block_pocket = protein.residues_to_pdb_block(
|
| 36 |
+
protein.query_residues_ligand(ligand_dict, radius)
|
| 37 |
+
)
|
| 38 |
+
pocket_dict = PDBProtein(pdb_block_pocket).to_dict_atom()
|
| 39 |
+
else:
|
| 40 |
+
pocket_dict = PDBProtein(pdb_path).to_dict_atom()
|
| 41 |
+
|
| 42 |
+
data = ProteinLigandData.from_protein_ligand_dicts(
|
| 43 |
+
protein_dict=torchify_dict(pocket_dict),
|
| 44 |
+
ligand_dict=torchify_dict(ligand_dict),
|
| 45 |
+
)
|
| 46 |
+
data.protein_filename = pdb_path
|
| 47 |
+
data.ligand_filename = ligand_path
|
| 48 |
+
assert data.protein_pos.size(0) > 0
|
| 49 |
+
if transform is not None:
|
| 50 |
+
data = transform(data)
|
| 51 |
+
return data
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def main():
|
| 55 |
+
parser = argparse.ArgumentParser()
|
| 56 |
+
parser.add_argument('--ckpt_path', type=str)
|
| 57 |
+
parser.add_argument('--protein_path', type=str)
|
| 58 |
+
parser.add_argument('--ligand_path', type=str)
|
| 59 |
+
parser.add_argument('--kind', type=str, default='Ki', choices=['Ki', 'Kd', 'IC50'])
|
| 60 |
+
parser.add_argument('--device', type=str, default='cuda')
|
| 61 |
+
parser.add_argument('--seed', type=int, default=2021)
|
| 62 |
+
args = parser.parse_args()
|
| 63 |
+
utils_misc.seed_all(args.seed)
|
| 64 |
+
|
| 65 |
+
# Logging
|
| 66 |
+
logger = utils_misc.get_logger('eval')
|
| 67 |
+
logger.info(args)
|
| 68 |
+
|
| 69 |
+
# Load config
|
| 70 |
+
logger.info(f'Loading model from {args.ckpt_path}')
|
| 71 |
+
ckpt_restore = torch.load(args.ckpt_path, map_location=torch.device('cpu'),weights_only=
|
| 72 |
+
False)
|
| 73 |
+
config = ckpt_restore['config']
|
| 74 |
+
logger.info(f'ckpt_config: {config}')
|
| 75 |
+
|
| 76 |
+
# Transforms
|
| 77 |
+
protein_featurizer = utils_trans.FeaturizeProteinAtom()
|
| 78 |
+
ligand_featurizer = utils_trans.FeaturizeLigandAtom()
|
| 79 |
+
transform = Compose([
|
| 80 |
+
protein_featurizer,
|
| 81 |
+
ligand_featurizer,
|
| 82 |
+
])
|
| 83 |
+
|
| 84 |
+
# Load model
|
| 85 |
+
model = get_model(config, protein_featurizer.feature_dim, ligand_featurizer.feature_dim)
|
| 86 |
+
model.load_state_dict(ckpt_restore['model'])
|
| 87 |
+
model = model.to(args.device)
|
| 88 |
+
# print(model)
|
| 89 |
+
logger.info(f'# trainable parameters: {utils_misc.count_parameters(model) / 1e6:.4f} M')
|
| 90 |
+
model.eval()
|
| 91 |
+
|
| 92 |
+
test_data = convert_data(args.protein_path, args.ligand_path, transform,
|
| 93 |
+
heavy_only=config.dataset.get('heavy_only', False))
|
| 94 |
+
test_data.kind = KMAP[args.kind]
|
| 95 |
+
test_set = InferenceDataset([test_data])
|
| 96 |
+
test_loader = DataLoader(test_set, batch_size=1, shuffle=False,
|
| 97 |
+
follow_batch=['protein_element', 'ligand_element'])
|
| 98 |
+
|
| 99 |
+
with torch.no_grad():
|
| 100 |
+
model.eval()
|
| 101 |
+
for batch in tqdm(test_loader, desc='Inference'):
|
| 102 |
+
batch = batch.to(args.device)
|
| 103 |
+
pred = model(
|
| 104 |
+
protein_pos=batch.protein_pos,
|
| 105 |
+
protein_atom_feature=batch.protein_atom_feature.float(),
|
| 106 |
+
ligand_pos=batch.ligand_pos,
|
| 107 |
+
ligand_atom_feature=batch.ligand_atom_feature_full.float(),
|
| 108 |
+
batch_protein=batch.protein_element_batch,
|
| 109 |
+
batch_ligand=batch.ligand_element_batch,
|
| 110 |
+
output_kind=batch.kind
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
print(f'PDB ID: {batch.protein_filename[0]} '
|
| 114 |
+
f'Prediction: {args.kind}={unit_transform(pred.cpu().squeeze()):.2e} m')
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def unit_transform(pka):
|
| 118 |
+
# pka = -log10 Kd / Ki
|
| 119 |
+
affinity = torch.pow(10, -pka.cpu().squeeze())
|
| 120 |
+
return affinity
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
main()
|
scripts/property_prediction/local_misc_prop.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from scipy.stats import pearsonr, spearmanr
|
| 3 |
+
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
|
| 4 |
+
from torch_geometric.loader import DataLoader
|
| 5 |
+
|
| 6 |
+
from models.property_pred.prop_model import PropPredNet, PropPredNetEnc
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_eval_scores(ypred_arr, ytrue_arr, logger, prefix='All'):
|
| 10 |
+
if len(ypred_arr) == 0:
|
| 11 |
+
return None
|
| 12 |
+
rmse = np.sqrt(mean_squared_error(ytrue_arr, ypred_arr))
|
| 13 |
+
mae = mean_absolute_error(ytrue_arr, ypred_arr)
|
| 14 |
+
r2 = r2_score(ytrue_arr, ypred_arr)
|
| 15 |
+
pearson, ppval = pearsonr(ytrue_arr, ypred_arr)
|
| 16 |
+
spearman, spval = spearmanr(ytrue_arr, ypred_arr)
|
| 17 |
+
mean = np.mean(ypred_arr)
|
| 18 |
+
std = np.std(ypred_arr)
|
| 19 |
+
logger.info("Evaluation Summary:")
|
| 20 |
+
logger.info(
|
| 21 |
+
"[%4s] num: %3d, RMSE: %.3f, MAE: %.3f, "
|
| 22 |
+
"R^2 score: %.3f, Pearson: %.3f, Spearman: %.3f, mean/std: %.3f/%.3f" % (
|
| 23 |
+
prefix, len(ypred_arr), rmse, mae, r2, pearson, spearman, mean, std))
|
| 24 |
+
return rmse
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def get_dataloader(train_set, val_set, test_set, config):
|
| 28 |
+
follow_batch = ['protein_element', 'ligand_element']
|
| 29 |
+
collate_exclude_keys = ['ligand_nbh_list']
|
| 30 |
+
train_loader = DataLoader(
|
| 31 |
+
train_set,
|
| 32 |
+
batch_size=config.train.batch_size,
|
| 33 |
+
shuffle=True,
|
| 34 |
+
num_workers=config.train.num_workers,
|
| 35 |
+
follow_batch=follow_batch,
|
| 36 |
+
exclude_keys=collate_exclude_keys
|
| 37 |
+
)
|
| 38 |
+
val_loader = DataLoader(val_set, config.train.batch_size, shuffle=False,
|
| 39 |
+
follow_batch=follow_batch, exclude_keys=collate_exclude_keys)
|
| 40 |
+
test_loader = DataLoader(test_set, config.train.batch_size, shuffle=False,
|
| 41 |
+
follow_batch=follow_batch, exclude_keys=collate_exclude_keys)
|
| 42 |
+
return train_loader, val_loader, test_loader
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def get_model(config, protein_atom_feat_dim, ligand_atom_feat_dim):
|
| 46 |
+
if config.model.encoder.name == 'egnn_enc':
|
| 47 |
+
model = PropPredNetEnc(
|
| 48 |
+
config.model,
|
| 49 |
+
protein_atom_feature_dim=protein_atom_feat_dim,
|
| 50 |
+
ligand_atom_feature_dim=ligand_atom_feat_dim,
|
| 51 |
+
enc_ligand_dim=config.model.enc_ligand_dim,
|
| 52 |
+
enc_node_dim=config.model.enc_node_dim,
|
| 53 |
+
enc_graph_dim=config.model.enc_graph_dim,
|
| 54 |
+
enc_feature_type=config.model.enc_feature_type,
|
| 55 |
+
output_dim=1
|
| 56 |
+
)
|
| 57 |
+
else:
|
| 58 |
+
model = PropPredNet(
|
| 59 |
+
config.model,
|
| 60 |
+
protein_atom_feature_dim=protein_atom_feat_dim,
|
| 61 |
+
ligand_atom_feature_dim=ligand_atom_feat_dim,
|
| 62 |
+
output_dim=3
|
| 63 |
+
)
|
| 64 |
+
return model
|
scripts/property_prediction/pdbbind_split.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
import argparse
|
| 5 |
+
import torch
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def coretest_split(index_path, test_path, val_ratio=0.1, val_num=None):
|
| 10 |
+
with open(index_path, 'rb') as f:
|
| 11 |
+
index = pickle.load(f)
|
| 12 |
+
test_ids = [f for f in os.listdir(test_path) if len(f) == 4]
|
| 13 |
+
all_ids = [os.path.basename(i[0])[:4] for i in index]
|
| 14 |
+
print('Original test set size: ', len(test_ids))
|
| 15 |
+
test_index = [all_ids.index(test_id) for test_id in test_ids if test_id in all_ids]
|
| 16 |
+
train_val_index = list(set(range(len(all_ids))) - set(test_index))
|
| 17 |
+
assert len(train_val_index) == len(all_ids) - len(test_index)
|
| 18 |
+
random.shuffle(train_val_index)
|
| 19 |
+
if val_num is not None:
|
| 20 |
+
n_val = val_num
|
| 21 |
+
else:
|
| 22 |
+
n_val = int(len(train_val_index) * val_ratio)
|
| 23 |
+
val_index = train_val_index[:n_val]
|
| 24 |
+
train_index = train_val_index[n_val:]
|
| 25 |
+
return train_index, val_index, test_index
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def time_split(index_path):
|
| 29 |
+
valid_ids = np.loadtxt("./data/pdbbind_v2020/timesplit_no_lig_overlap_val", dtype=str)
|
| 30 |
+
test_ids = np.loadtxt("./data/pdbbind_v2020/timesplit_test", dtype=str)
|
| 31 |
+
|
| 32 |
+
with open(index_path, 'rb') as f:
|
| 33 |
+
index = pickle.load(f)
|
| 34 |
+
all_ids = [os.path.basename(i[0])[:4] for i in index]
|
| 35 |
+
val_index = [all_ids.index(val_id) for val_id in valid_ids if val_id in all_ids]
|
| 36 |
+
test_index = [all_ids.index(test_id) for test_id in test_ids if test_id in all_ids]
|
| 37 |
+
train_index = list(set(range(len(all_ids))) - set(test_index) - set(val_index))
|
| 38 |
+
return train_index, val_index, test_index
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
if __name__ == '__main__':
|
| 42 |
+
parser = argparse.ArgumentParser()
|
| 43 |
+
parser.add_argument('--index_path', type=str, default='./data/pdbbind_v2016/pocket_10/index.pkl')
|
| 44 |
+
parser.add_argument('--split_mode', type=str, choices=['coreset', 'time'], default='coreset')
|
| 45 |
+
parser.add_argument('--test_path', type=str, default='./data/pdbbind_v2016/coreset')
|
| 46 |
+
parser.add_argument('--val_ratio', type=float, default=0.1)
|
| 47 |
+
parser.add_argument('--val_num', type=int, default=None)
|
| 48 |
+
parser.add_argument('--save_path', type=str, default='./data/pdbbind_v2016/pocket_10/split.pt')
|
| 49 |
+
parser.add_argument('--seed', type=int, default=2021)
|
| 50 |
+
args = parser.parse_args()
|
| 51 |
+
random.seed(args.seed)
|
| 52 |
+
|
| 53 |
+
if args.split_mode == 'coreset':
|
| 54 |
+
train_index, val_index, test_index = coretest_split(args.index_path, args.test_path,
|
| 55 |
+
args.val_ratio, args.val_num)
|
| 56 |
+
elif args.split_mode == 'time':
|
| 57 |
+
train_index, val_index, test_index = time_split(args.index_path)
|
| 58 |
+
else:
|
| 59 |
+
raise ValueError(args.split_mode)
|
| 60 |
+
torch.save({
|
| 61 |
+
'train': train_index,
|
| 62 |
+
'val': val_index,
|
| 63 |
+
'test': test_index
|
| 64 |
+
}, args.save_path)
|
| 65 |
+
print('Train %d, Validation %d, Test %d.' % (len(train_index), len(val_index), len(test_index)))
|
| 66 |
+
print('Done.')
|
scripts/property_prediction/train_prop.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import argparse
|
| 4 |
+
from tqdm.auto import tqdm
|
| 5 |
+
import torch
|
| 6 |
+
from torch.nn.utils import clip_grad_norm_
|
| 7 |
+
import torch.utils.tensorboard
|
| 8 |
+
import yaml
|
| 9 |
+
from torch_geometric.transforms import Compose
|
| 10 |
+
|
| 11 |
+
from onescience.datapipes.targetdiff import get_dataset
|
| 12 |
+
import onescience.utils.targetdiff.transforms_prop as utils_trans
|
| 13 |
+
import onescience.utils.targetdiff.misc as utils_misc
|
| 14 |
+
from onescience.utils.targetdiff.train import get_scheduler, get_optimizer
|
| 15 |
+
import numpy as np
|
| 16 |
+
from onescience.datapipes.targetdiff.protein_ligand import KMAP
|
| 17 |
+
from scripts.property_prediction.local_misc_prop import get_model, get_dataloader, get_eval_scores
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', '..'))
|
| 21 |
+
MODELS_SNAPSHOT_SRC = os.path.join(REPO_ROOT, 'src', 'onescience', 'models', 'targetdiff')
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def parse_override_value(raw_value, old_value):
|
| 25 |
+
parsed_value = yaml.safe_load(raw_value)
|
| 26 |
+
if old_value is None:
|
| 27 |
+
return parsed_value
|
| 28 |
+
if isinstance(old_value, bool):
|
| 29 |
+
if isinstance(parsed_value, bool):
|
| 30 |
+
return parsed_value
|
| 31 |
+
return str(parsed_value).lower() in ('1', 'true', 'yes', 'y')
|
| 32 |
+
if isinstance(old_value, tuple):
|
| 33 |
+
if isinstance(parsed_value, str):
|
| 34 |
+
return tuple(item.strip() for item in parsed_value.split(','))
|
| 35 |
+
return tuple(parsed_value)
|
| 36 |
+
if isinstance(old_value, list):
|
| 37 |
+
if isinstance(parsed_value, str):
|
| 38 |
+
return [item.strip() for item in parsed_value.split(',')]
|
| 39 |
+
return list(parsed_value)
|
| 40 |
+
return type(old_value)(parsed_value)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def apply_config_overrides(config, overrides):
|
| 44 |
+
if len(overrides) % 2 != 0:
|
| 45 |
+
raise ValueError('Config overrides must use "--key value" pairs.')
|
| 46 |
+
|
| 47 |
+
for key_arg, raw_value in zip(overrides[::2], overrides[1::2]):
|
| 48 |
+
if not key_arg.startswith('--'):
|
| 49 |
+
raise ValueError(f'Config override key must start with "--": {key_arg}')
|
| 50 |
+
|
| 51 |
+
key_path = key_arg[2:]
|
| 52 |
+
parts = key_path.split('.')
|
| 53 |
+
node = config
|
| 54 |
+
for part in parts[:-1]:
|
| 55 |
+
if part not in node:
|
| 56 |
+
raise KeyError(f'Unknown config override: {key_path}')
|
| 57 |
+
node = node[part]
|
| 58 |
+
|
| 59 |
+
leaf = parts[-1]
|
| 60 |
+
if leaf not in node:
|
| 61 |
+
raise KeyError(f'Unknown config override: {key_path}')
|
| 62 |
+
node[leaf] = parse_override_value(raw_value, node[leaf])
|
| 63 |
+
|
| 64 |
+
return config
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def main():
|
| 68 |
+
parser = argparse.ArgumentParser()
|
| 69 |
+
parser.add_argument('config', type=str)
|
| 70 |
+
parser.add_argument('--device', type=str, default='cuda')
|
| 71 |
+
parser.add_argument('--logdir', type=str, default='./logs')
|
| 72 |
+
parser.add_argument('--tag', type=str, default='')
|
| 73 |
+
args, config_overrides = parser.parse_known_args()
|
| 74 |
+
|
| 75 |
+
# Load configs
|
| 76 |
+
config = utils_misc.load_config(args.config)
|
| 77 |
+
config = apply_config_overrides(config, config_overrides)
|
| 78 |
+
config_name = os.path.basename(args.config)[:os.path.basename(args.config).rfind('.')]
|
| 79 |
+
utils_misc.seed_all(config.train.seed)
|
| 80 |
+
|
| 81 |
+
# Logging
|
| 82 |
+
log_dir = utils_misc.get_new_log_dir(args.logdir, prefix=config_name, tag=args.tag)
|
| 83 |
+
ckpt_dir = os.path.join(log_dir, 'checkpoints')
|
| 84 |
+
os.makedirs(ckpt_dir, exist_ok=True)
|
| 85 |
+
logger = utils_misc.get_logger('train', log_dir)
|
| 86 |
+
writer = torch.utils.tensorboard.SummaryWriter(log_dir)
|
| 87 |
+
logger.info(args)
|
| 88 |
+
logger.info(config)
|
| 89 |
+
shutil.copyfile(args.config, os.path.join(log_dir, os.path.basename(args.config)))
|
| 90 |
+
local_models_dir = './models'
|
| 91 |
+
if os.path.isdir(local_models_dir):
|
| 92 |
+
shutil.copytree(local_models_dir, os.path.join(log_dir, 'models'))
|
| 93 |
+
elif os.path.isdir(MODELS_SNAPSHOT_SRC):
|
| 94 |
+
shutil.copytree(MODELS_SNAPSHOT_SRC, os.path.join(log_dir, 'models'))
|
| 95 |
+
else:
|
| 96 |
+
logger.warning('Skip model source snapshot: no ./models or migrated TargetDiff models directory found.')
|
| 97 |
+
|
| 98 |
+
# Transforms
|
| 99 |
+
protein_featurizer = utils_trans.FeaturizeProteinAtom()
|
| 100 |
+
ligand_featurizer = utils_trans.FeaturizeLigandAtom()
|
| 101 |
+
transform = Compose([
|
| 102 |
+
protein_featurizer,
|
| 103 |
+
ligand_featurizer,
|
| 104 |
+
])
|
| 105 |
+
|
| 106 |
+
# Datasets and loaders
|
| 107 |
+
logger.info('Loading dataset...')
|
| 108 |
+
dataset, subsets = get_dataset(
|
| 109 |
+
config=config.dataset,
|
| 110 |
+
transform=transform,
|
| 111 |
+
emb_path=config.dataset.emb_path if 'emb_path' in config.dataset else None,
|
| 112 |
+
heavy_only=config.dataset.heavy_only
|
| 113 |
+
)
|
| 114 |
+
train_set, val_set, test_set = subsets['train'], subsets['val'], subsets['test']
|
| 115 |
+
logger.info(f'Train set: {len(train_set)} Val set: {len(val_set)} Test set: {len(test_set)}')
|
| 116 |
+
train_loader, val_loader, test_loader = get_dataloader(train_set, val_set, test_set, config)
|
| 117 |
+
# Model
|
| 118 |
+
logger.info('Building model...')
|
| 119 |
+
model = get_model(config, protein_featurizer.feature_dim, ligand_featurizer.feature_dim)
|
| 120 |
+
model = model.to(args.device)
|
| 121 |
+
logger.info(f'# trainable parameters: {utils_misc.count_parameters(model) / 1e6:.4f} M')
|
| 122 |
+
|
| 123 |
+
# Optimizer and scheduler
|
| 124 |
+
optimizer = get_optimizer(config.train.optimizer, model)
|
| 125 |
+
scheduler = get_scheduler(config.train.scheduler, optimizer)
|
| 126 |
+
|
| 127 |
+
def train(epoch):
|
| 128 |
+
model.train()
|
| 129 |
+
optimizer.zero_grad()
|
| 130 |
+
it = 0
|
| 131 |
+
num_it = len(train_loader)
|
| 132 |
+
for batch in tqdm(train_loader, dynamic_ncols=True, desc=f'Epoch {epoch}', position=1):
|
| 133 |
+
it += 1
|
| 134 |
+
batch = batch.to(args.device)
|
| 135 |
+
# compute loss
|
| 136 |
+
loss = model.get_loss(batch, pos_noise_std=config.train.pos_noise_std)
|
| 137 |
+
loss.backward()
|
| 138 |
+
orig_grad_norm = clip_grad_norm_(model.parameters(), config.train.max_grad_norm)
|
| 139 |
+
optimizer.step()
|
| 140 |
+
optimizer.zero_grad()
|
| 141 |
+
|
| 142 |
+
if it % config.train.report_iter == 0:
|
| 143 |
+
logger.info('[Train] Epoch %03d Iter %04d | Loss %.6f | Lr %.4f * 1e-3' % (
|
| 144 |
+
epoch, it, loss.item(), optimizer.param_groups[0]['lr'] * 1000
|
| 145 |
+
))
|
| 146 |
+
|
| 147 |
+
writer.add_scalar('train/loss', loss, it + epoch * num_it)
|
| 148 |
+
writer.add_scalar('train/lr', optimizer.param_groups[0]['lr'], it + epoch * num_it)
|
| 149 |
+
writer.add_scalar('train/grad', orig_grad_norm, it + epoch * num_it)
|
| 150 |
+
writer.flush()
|
| 151 |
+
|
| 152 |
+
def validate(epoch, data_loader, scheduler, writer, prefix='Validate'):
|
| 153 |
+
sum_loss, sum_n = 0, 0
|
| 154 |
+
ytrue_arr, ypred_arr = [], []
|
| 155 |
+
y_kind = []
|
| 156 |
+
with torch.no_grad():
|
| 157 |
+
model.eval()
|
| 158 |
+
for batch in tqdm(data_loader, desc=prefix):
|
| 159 |
+
batch = batch.to(args.device)
|
| 160 |
+
loss, pred = model.get_loss(batch, pos_noise_std=0., return_pred=True)
|
| 161 |
+
sum_loss += loss.item() * len(batch.y)
|
| 162 |
+
sum_n += len(batch.y)
|
| 163 |
+
ypred_arr.append(pred.view(-1))
|
| 164 |
+
ytrue_arr.append(batch.y)
|
| 165 |
+
y_kind.append(batch.kind)
|
| 166 |
+
avg_loss = sum_loss / sum_n
|
| 167 |
+
logger.info('[%s] Epoch %03d | Loss %.6f' % (
|
| 168 |
+
prefix, epoch, avg_loss,
|
| 169 |
+
))
|
| 170 |
+
ypred_arr = torch.cat(ypred_arr).cpu().numpy().astype(np.float64)
|
| 171 |
+
ytrue_arr = torch.cat(ytrue_arr).cpu().numpy().astype(np.float64)
|
| 172 |
+
y_kind = torch.cat(y_kind).cpu().numpy()
|
| 173 |
+
rmse = get_eval_scores(ypred_arr, ytrue_arr, logger)
|
| 174 |
+
for k, v in KMAP.items():
|
| 175 |
+
get_eval_scores(ypred_arr[y_kind == v], ytrue_arr[y_kind == v], logger, prefix=k)
|
| 176 |
+
|
| 177 |
+
if scheduler:
|
| 178 |
+
if config.train.scheduler.type == 'plateau':
|
| 179 |
+
scheduler.step(avg_loss)
|
| 180 |
+
elif config.train.scheduler.type == 'warmup_plateau':
|
| 181 |
+
scheduler.step_ReduceLROnPlateau(avg_loss)
|
| 182 |
+
else:
|
| 183 |
+
scheduler.step()
|
| 184 |
+
|
| 185 |
+
if writer:
|
| 186 |
+
writer.add_scalar('val/loss', avg_loss, epoch)
|
| 187 |
+
writer.add_scalar('val/rmse', rmse, epoch)
|
| 188 |
+
writer.flush()
|
| 189 |
+
|
| 190 |
+
return avg_loss
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
best_val_loss = float('inf')
|
| 194 |
+
best_val_epoch = 0
|
| 195 |
+
patience = 0
|
| 196 |
+
for epoch in range(1, config.train.max_epochs + 1):
|
| 197 |
+
# with torch.autograd.detect_anomaly():
|
| 198 |
+
train(epoch)
|
| 199 |
+
if epoch % config.train.val_freq == 0 or epoch == config.train.max_epochs:
|
| 200 |
+
val_loss = validate(epoch, val_loader, scheduler, writer)
|
| 201 |
+
validate(epoch, test_loader, scheduler=None, writer=None, prefix='Test')
|
| 202 |
+
|
| 203 |
+
if val_loss < best_val_loss:
|
| 204 |
+
patience = 0
|
| 205 |
+
best_val_loss = val_loss
|
| 206 |
+
best_val_epoch = epoch
|
| 207 |
+
logger.info(f'Best val achieved at epoch {epoch}, val loss: {best_val_loss:.3f}')
|
| 208 |
+
logger.info(f'Eval on Test set:')
|
| 209 |
+
validate(epoch, test_loader, scheduler=None, writer=None, prefix='Test')
|
| 210 |
+
ckpt_path = os.path.join(ckpt_dir, '%d.pt' % epoch)
|
| 211 |
+
torch.save({
|
| 212 |
+
'config': config,
|
| 213 |
+
'model': model.state_dict(),
|
| 214 |
+
'optimizer': optimizer.state_dict(),
|
| 215 |
+
'scheduler': scheduler.state_dict(),
|
| 216 |
+
'epoch': epoch,
|
| 217 |
+
}, ckpt_path)
|
| 218 |
+
logger.info(f'Model {log_dir}/{epoch}.pt saved!')
|
| 219 |
+
else:
|
| 220 |
+
patience += 1
|
| 221 |
+
logger.info(f'Val loss does not improve, patience: {patience} '
|
| 222 |
+
f'(Best val loss: {best_val_loss:.3f} at epoch {best_val_epoch})')
|
| 223 |
+
|
| 224 |
+
except KeyboardInterrupt:
|
| 225 |
+
logger.info('Terminating...')
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
if __name__ == '__main__':
|
| 229 |
+
main()
|
scripts/sample_diffusion.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
import time
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from torch_geometric.data import Batch
|
| 9 |
+
from torch_geometric.transforms import Compose
|
| 10 |
+
from torch_scatter import scatter_sum, scatter_mean
|
| 11 |
+
from tqdm.auto import tqdm
|
| 12 |
+
|
| 13 |
+
import onescience.utils.targetdiff.misc as misc
|
| 14 |
+
import onescience.utils.targetdiff.transforms as trans
|
| 15 |
+
from onescience.datapipes.targetdiff import get_dataset
|
| 16 |
+
from onescience.datapipes.targetdiff.pl_data import FOLLOW_BATCH
|
| 17 |
+
from models.molopt_score_model import ScorePosNet3D, log_sample_categorical
|
| 18 |
+
from onescience.utils.targetdiff.evaluation import atom_num
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def unbatch_v_traj(ligand_v_traj, n_data, ligand_cum_atoms):
|
| 22 |
+
all_step_v = [[] for _ in range(n_data)]
|
| 23 |
+
for v in ligand_v_traj: # step_i
|
| 24 |
+
v_array = v.cpu().numpy()
|
| 25 |
+
for k in range(n_data):
|
| 26 |
+
all_step_v[k].append(v_array[ligand_cum_atoms[k]:ligand_cum_atoms[k + 1]])
|
| 27 |
+
all_step_v = [np.stack(step_v) for step_v in all_step_v] # num_samples * [num_steps, num_atoms_i]
|
| 28 |
+
return all_step_v
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def sample_diffusion_ligand(model, data, num_samples, batch_size=16, device='cuda:0',
|
| 32 |
+
num_steps=None, pos_only=False, center_pos_mode='protein',
|
| 33 |
+
sample_num_atoms='prior'):
|
| 34 |
+
all_pred_pos, all_pred_v = [], []
|
| 35 |
+
all_pred_pos_traj, all_pred_v_traj = [], []
|
| 36 |
+
all_pred_v0_traj, all_pred_vt_traj = [], []
|
| 37 |
+
time_list = []
|
| 38 |
+
num_batch = int(np.ceil(num_samples / batch_size))
|
| 39 |
+
current_i = 0
|
| 40 |
+
for i in tqdm(range(num_batch)):
|
| 41 |
+
n_data = batch_size if i < num_batch - 1 else num_samples - batch_size * (num_batch - 1)
|
| 42 |
+
batch = Batch.from_data_list([data.clone() for _ in range(n_data)], follow_batch=FOLLOW_BATCH).to(device)
|
| 43 |
+
|
| 44 |
+
t1 = time.time()
|
| 45 |
+
with torch.no_grad():
|
| 46 |
+
batch_protein = batch.protein_element_batch
|
| 47 |
+
if sample_num_atoms == 'prior':
|
| 48 |
+
pocket_size = atom_num.get_space_size(data.protein_pos.detach().cpu().numpy())
|
| 49 |
+
ligand_num_atoms = [atom_num.sample_atom_num(pocket_size).astype(int) for _ in range(n_data)]
|
| 50 |
+
batch_ligand = torch.repeat_interleave(torch.arange(n_data), torch.tensor(ligand_num_atoms)).to(device)
|
| 51 |
+
elif sample_num_atoms == 'range':
|
| 52 |
+
ligand_num_atoms = list(range(current_i + 1, current_i + n_data + 1))
|
| 53 |
+
batch_ligand = torch.repeat_interleave(torch.arange(n_data), torch.tensor(ligand_num_atoms)).to(device)
|
| 54 |
+
elif sample_num_atoms == 'ref':
|
| 55 |
+
batch_ligand = batch.ligand_element_batch
|
| 56 |
+
ligand_num_atoms = scatter_sum(torch.ones_like(batch_ligand), batch_ligand, dim=0).tolist()
|
| 57 |
+
else:
|
| 58 |
+
raise ValueError
|
| 59 |
+
|
| 60 |
+
# init ligand pos
|
| 61 |
+
center_pos = scatter_mean(batch.protein_pos, batch_protein, dim=0)
|
| 62 |
+
batch_center_pos = center_pos[batch_ligand]
|
| 63 |
+
init_ligand_pos = batch_center_pos + torch.randn_like(batch_center_pos)
|
| 64 |
+
|
| 65 |
+
# init ligand v
|
| 66 |
+
if pos_only:
|
| 67 |
+
init_ligand_v = batch.ligand_atom_feature_full
|
| 68 |
+
else:
|
| 69 |
+
uniform_logits = torch.zeros(len(batch_ligand), model.num_classes).to(device)
|
| 70 |
+
init_ligand_v = log_sample_categorical(uniform_logits)
|
| 71 |
+
|
| 72 |
+
r = model.sample_diffusion(
|
| 73 |
+
protein_pos=batch.protein_pos,
|
| 74 |
+
protein_v=batch.protein_atom_feature.float(),
|
| 75 |
+
batch_protein=batch_protein,
|
| 76 |
+
|
| 77 |
+
init_ligand_pos=init_ligand_pos,
|
| 78 |
+
init_ligand_v=init_ligand_v,
|
| 79 |
+
batch_ligand=batch_ligand,
|
| 80 |
+
num_steps=num_steps,
|
| 81 |
+
pos_only=pos_only,
|
| 82 |
+
center_pos_mode=center_pos_mode
|
| 83 |
+
)
|
| 84 |
+
ligand_pos, ligand_v, ligand_pos_traj, ligand_v_traj = r['pos'], r['v'], r['pos_traj'], r['v_traj']
|
| 85 |
+
ligand_v0_traj, ligand_vt_traj = r['v0_traj'], r['vt_traj']
|
| 86 |
+
# unbatch pos
|
| 87 |
+
ligand_cum_atoms = np.cumsum([0] + ligand_num_atoms)
|
| 88 |
+
ligand_pos_array = ligand_pos.cpu().numpy().astype(np.float64)
|
| 89 |
+
all_pred_pos += [ligand_pos_array[ligand_cum_atoms[k]:ligand_cum_atoms[k + 1]] for k in
|
| 90 |
+
range(n_data)] # num_samples * [num_atoms_i, 3]
|
| 91 |
+
|
| 92 |
+
all_step_pos = [[] for _ in range(n_data)]
|
| 93 |
+
for p in ligand_pos_traj: # step_i
|
| 94 |
+
p_array = p.cpu().numpy().astype(np.float64)
|
| 95 |
+
for k in range(n_data):
|
| 96 |
+
all_step_pos[k].append(p_array[ligand_cum_atoms[k]:ligand_cum_atoms[k + 1]])
|
| 97 |
+
all_step_pos = [np.stack(step_pos) for step_pos in
|
| 98 |
+
all_step_pos] # num_samples * [num_steps, num_atoms_i, 3]
|
| 99 |
+
all_pred_pos_traj += [p for p in all_step_pos]
|
| 100 |
+
|
| 101 |
+
# unbatch v
|
| 102 |
+
ligand_v_array = ligand_v.cpu().numpy()
|
| 103 |
+
all_pred_v += [ligand_v_array[ligand_cum_atoms[k]:ligand_cum_atoms[k + 1]] for k in range(n_data)]
|
| 104 |
+
|
| 105 |
+
all_step_v = unbatch_v_traj(ligand_v_traj, n_data, ligand_cum_atoms)
|
| 106 |
+
all_pred_v_traj += [v for v in all_step_v]
|
| 107 |
+
|
| 108 |
+
if not pos_only:
|
| 109 |
+
all_step_v0 = unbatch_v_traj(ligand_v0_traj, n_data, ligand_cum_atoms)
|
| 110 |
+
all_pred_v0_traj += [v for v in all_step_v0]
|
| 111 |
+
all_step_vt = unbatch_v_traj(ligand_vt_traj, n_data, ligand_cum_atoms)
|
| 112 |
+
all_pred_vt_traj += [v for v in all_step_vt]
|
| 113 |
+
t2 = time.time()
|
| 114 |
+
time_list.append(t2 - t1)
|
| 115 |
+
current_i += n_data
|
| 116 |
+
return all_pred_pos, all_pred_v, all_pred_pos_traj, all_pred_v_traj, all_pred_v0_traj, all_pred_vt_traj, time_list
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == '__main__':
|
| 120 |
+
parser = argparse.ArgumentParser()
|
| 121 |
+
parser.add_argument('config', type=str)
|
| 122 |
+
parser.add_argument('-i', '--data_id', type=int)
|
| 123 |
+
parser.add_argument('--device', type=str, default='cuda:0')
|
| 124 |
+
parser.add_argument('--batch_size', type=int, default=100)
|
| 125 |
+
parser.add_argument('--result_path', type=str, default='./outputs')
|
| 126 |
+
args = parser.parse_args()
|
| 127 |
+
|
| 128 |
+
logger = misc.get_logger('sampling')
|
| 129 |
+
|
| 130 |
+
# Load config
|
| 131 |
+
config = misc.load_config(args.config)
|
| 132 |
+
logger.info(config)
|
| 133 |
+
misc.seed_all(config.sample.seed)
|
| 134 |
+
|
| 135 |
+
# Load checkpoint
|
| 136 |
+
ckpt = torch.load(config.model.checkpoint, map_location=args.device)
|
| 137 |
+
logger.info(f"Training Config: {ckpt['config']}")
|
| 138 |
+
|
| 139 |
+
# Transforms
|
| 140 |
+
protein_featurizer = trans.FeaturizeProteinAtom()
|
| 141 |
+
ligand_atom_mode = ckpt['config'].data.transform.ligand_atom_mode
|
| 142 |
+
ligand_featurizer = trans.FeaturizeLigandAtom(ligand_atom_mode)
|
| 143 |
+
transform = Compose([
|
| 144 |
+
protein_featurizer,
|
| 145 |
+
ligand_featurizer,
|
| 146 |
+
trans.FeaturizeLigandBond(),
|
| 147 |
+
])
|
| 148 |
+
|
| 149 |
+
# Load dataset
|
| 150 |
+
dataset, subsets = get_dataset(
|
| 151 |
+
config=ckpt['config'].data,
|
| 152 |
+
transform=transform
|
| 153 |
+
)
|
| 154 |
+
train_set, test_set = subsets['train'], subsets['test']
|
| 155 |
+
logger.info(f'Successfully load the dataset (size: {len(test_set)})!')
|
| 156 |
+
|
| 157 |
+
# Load model
|
| 158 |
+
model = ScorePosNet3D(
|
| 159 |
+
ckpt['config'].model,
|
| 160 |
+
protein_atom_feature_dim=protein_featurizer.feature_dim,
|
| 161 |
+
ligand_atom_feature_dim=ligand_featurizer.feature_dim
|
| 162 |
+
).to(args.device)
|
| 163 |
+
model.load_state_dict(ckpt['model'])
|
| 164 |
+
logger.info(f'Successfully load the model! {config.model.checkpoint}')
|
| 165 |
+
|
| 166 |
+
data = test_set[args.data_id]
|
| 167 |
+
pred_pos, pred_v, pred_pos_traj, pred_v_traj, pred_v0_traj, pred_vt_traj, time_list = sample_diffusion_ligand(
|
| 168 |
+
model, data, config.sample.num_samples,
|
| 169 |
+
batch_size=args.batch_size, device=args.device,
|
| 170 |
+
num_steps=config.sample.num_steps,
|
| 171 |
+
pos_only=config.sample.pos_only,
|
| 172 |
+
center_pos_mode=config.sample.center_pos_mode,
|
| 173 |
+
sample_num_atoms=config.sample.sample_num_atoms
|
| 174 |
+
)
|
| 175 |
+
result = {
|
| 176 |
+
'data': data,
|
| 177 |
+
'pred_ligand_pos': pred_pos,
|
| 178 |
+
'pred_ligand_v': pred_v,
|
| 179 |
+
'pred_ligand_pos_traj': pred_pos_traj,
|
| 180 |
+
'pred_ligand_v_traj': pred_v_traj,
|
| 181 |
+
'time': time_list
|
| 182 |
+
}
|
| 183 |
+
logger.info('Sample done!')
|
| 184 |
+
|
| 185 |
+
result_path = args.result_path
|
| 186 |
+
os.makedirs(result_path, exist_ok=True)
|
| 187 |
+
shutil.copyfile(args.config, os.path.join(result_path, 'sample.yml'))
|
| 188 |
+
torch.save(result, os.path.join(result_path, f'result_{args.data_id}.pt'))
|