anzhi2710gmailcom commited on
Commit
3ac1d94
·
verified ·
1 Parent(s): 87652d1

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +25 -10
  2. README.md +387 -0
  3. configs/prop/pdbbind_general_egnn.yml +39 -0
  4. configs/prop/pdbbind_general_egnn_enc_final_h.yml +44 -0
  5. configs/sampling.yml +10 -0
  6. configs/training.yml +64 -0
  7. examples/3ug2_ligand.sdf +136 -0
  8. examples/3ug2_protein.pdb +0 -0
  9. models/__init__.py +2 -0
  10. models/__pycache__/__init__.cpython-311.pyc +0 -0
  11. models/__pycache__/common.cpython-311.pyc +0 -0
  12. models/__pycache__/egnn.cpython-311.pyc +0 -0
  13. models/__pycache__/molopt_score_model.cpython-311.pyc +0 -0
  14. models/__pycache__/uni_transformer.cpython-311.pyc +0 -0
  15. models/common.py +212 -0
  16. models/egnn.py +133 -0
  17. models/molopt_score_model.py +708 -0
  18. models/property_pred/__init__.py +2 -0
  19. models/property_pred/__pycache__/__init__.cpython-311.pyc +0 -0
  20. models/property_pred/__pycache__/prop_egnn.cpython-311.pyc +0 -0
  21. models/property_pred/__pycache__/prop_model.cpython-311.pyc +0 -0
  22. models/property_pred/prop_egnn.py +83 -0
  23. models/property_pred/prop_model.py +212 -0
  24. models/uni_transformer.py +328 -0
  25. scripts/__init__.py +2 -0
  26. scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  27. scripts/__pycache__/sample_diffusion.cpython-311.pyc +0 -0
  28. scripts/__pycache__/sample_for_pocket.cpython-311.pyc +0 -0
  29. scripts/batch_sample_diffusion.sh +21 -0
  30. scripts/data_preparation/__init__.py +2 -0
  31. scripts/data_preparation/clean_crossdocked.py +59 -0
  32. scripts/data_preparation/extract_pockets.py +76 -0
  33. scripts/data_preparation/split_pl_dataset.py +112 -0
  34. scripts/dock_baseline.py +59 -0
  35. scripts/dock_testset.py +76 -0
  36. scripts/evaluate_diffusion.py +208 -0
  37. scripts/evaluate_from_meta.py +138 -0
  38. scripts/inference.sh +27 -0
  39. scripts/likelihood_est_diffusion.py +198 -0
  40. scripts/property_prediction/__init__.py +2 -0
  41. scripts/property_prediction/__pycache__/__init__.cpython-311.pyc +0 -0
  42. scripts/property_prediction/__pycache__/local_misc_prop.cpython-311.pyc +0 -0
  43. scripts/property_prediction/eval_prop.py +93 -0
  44. scripts/property_prediction/extract_pockets.py +121 -0
  45. scripts/property_prediction/fixed_inference.py +225 -0
  46. scripts/property_prediction/inference.py +124 -0
  47. scripts/property_prediction/local_misc_prop.py +64 -0
  48. scripts/property_prediction/pdbbind_split.py +66 -0
  49. scripts/property_prediction/train_prop.py +229 -0
  50. 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
- *.pickle filter=lfs diff=lfs merge=lfs -text
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'))