Spaces:
Build error
Build error
Initial commit
Browse files- README.md +72 -14
- app.py +90 -0
- code/VHSE.csv +21 -0
- code/__init__.py +0 -0
- code/data_loader.py +194 -0
- code/data_preprocessing/__init__.py +7 -0
- code/data_preprocessing/model.py +113 -0
- code/data_preprocessing/process.py +136 -0
- code/model_py/__init__.py +14 -0
- code/model_py/utils.py +67 -0
- code/model_py/v21_2.py +503 -0
- code/predictor.py +60 -0
- model_weights.pth +3 -0
- requirements.in +8 -0
- requirements.txt +47 -0
- wrapper.py +144 -0
README.md
CHANGED
|
@@ -1,14 +1,72 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## Installation
|
| 2 |
+
|
| 3 |
+
Clone the repository `???`.
|
| 4 |
+
Then, extract the [FoldX](https://foldxsuite.crg.eu/products#foldx) distribution for your operating system into `external/foldx` in the cloned project folder, _make sure_ the binary is named `foldx`.
|
| 5 |
+
Finally, install the dependencies and check the installation works:
|
| 6 |
+
```bash
|
| 7 |
+
pip install -r requirements.in
|
| 8 |
+
python -m tests.inference
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
If the installation *fails*, or the application *crashes*, try to install *pinned dependencies* instead: `pip install -r requirements.txt`. Also check the table below.
|
| 12 |
+
|
| 13 |
+
#### Tested configurations:
|
| 14 |
+
|
| 15 |
+
| OS | Python | PyTorch | Engine | Project version |
|
| 16 |
+
|-------|--------|---------|-----------|--------------------------------|
|
| 17 |
+
| Win 10 | 3.8.18 | 2.1.2 | CPU | January 16, 2024 |
|
| 18 |
+
| Linux | 3.9.19 | 2.1.2 | CPU | January 16, 2024 |
|
| 19 |
+
| Linux | 3.8.13 | 1.10.0 | CUDA 11.3 | January 16, 2024 |
|
| 20 |
+
|
| 21 |
+
### Usage
|
| 22 |
+
|
| 23 |
+
```
|
| 24 |
+
wrapper.py [-h] [-v] pdb-code [chain] wild-type location mutation
|
| 25 |
+
|
| 26 |
+
Solubility change preditor:
|
| 27 |
+
Predicts the change in the solubility of a given protein variant
|
| 28 |
+
|
| 29 |
+
positional arguments:
|
| 30 |
+
pdb-code PDB code
|
| 31 |
+
chain target chain character; default=A
|
| 32 |
+
wild-type wild-type residue(s) amino-acid[n]
|
| 33 |
+
location mutated position(s) integer[n]
|
| 34 |
+
mutation 1 or n substituents amino-acid|amino-acid[n]
|
| 35 |
+
|
| 36 |
+
optional arguments:
|
| 37 |
+
-h, --help show this help message and exit
|
| 38 |
+
-v, --verbose
|
| 39 |
+
|
| 40 |
+
Amino acids can be specified by both 1- or 3-letter code.
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Example usage (double-point mutant in [erythropoietin](https://www.rcsb.org/sequence/1EER))`and its outpus:
|
| 44 |
+
```bash
|
| 45 |
+
./wrapper.py 1EER F,R 48,150 D --verbose
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
Predicted solubility change: 5.000398e-01 (solubilizing)
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Development
|
| 53 |
+
|
| 54 |
+
### Reproducible environment
|
| 55 |
+
|
| 56 |
+
`requirements.in` – project (direct) dependencies required for *inference*. Keep the PyTorch version specified here in sync with the link for the *pyg* wheels specified ibidem.
|
| 57 |
+
|
| 58 |
+
`requirements.txt` – _resolved_ and _pinned_ dependencies based on the file above, a result of `pip install -r requirements.in && pip freeze` in a fresh Python environment.
|
| 59 |
+
note: `pip-compile` of pip-tools does not seem to work with complex PyTorch ecosystem
|
| 60 |
+
|
| 61 |
+
### Style guide
|
| 62 |
+
#### Indentation
|
| 63 |
+
Use tabs `\t` for an indentation, spaces (after tabs) for an alignment ([reasoning](https://stackoverflow.com/a/5048130/1908192), [reasoning2](https://www.reddit.com/r/javascript/comments/c8drjo/nobody_talks_about_the_real_reason_to_use_tabs/
|
| 64 |
+
))
|
| 65 |
+
|
| 66 |
+
#### Naming conventions (PEP8-based)
|
| 67 |
+
module_name, CONSTANT_NAME, ClassName (capitalize first letters of words), subroutine_name, variable_name
|
| 68 |
+
|
| 69 |
+
ClassName, function_name: keep acronyms in their original case: GetPDB, get_PDB
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
app.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from wrapper import *
|
| 3 |
+
|
| 4 |
+
# --- Helper functions from wrapper.py ---
|
| 5 |
+
def parse_amino_acid(value):
|
| 6 |
+
value = value.upper()
|
| 7 |
+
if len(value) == 1:
|
| 8 |
+
try:
|
| 9 |
+
value = AA.one_to_three(value)
|
| 10 |
+
except Exception:
|
| 11 |
+
pass
|
| 12 |
+
if not AA.is_aa(value):
|
| 13 |
+
raise ValueError(f"'{value}' is not a valid amino acid")
|
| 14 |
+
return AA.three_to_one(value.upper())
|
| 15 |
+
|
| 16 |
+
def predict_solubility(pdb_code, chain, orig, loc, mut, version=None):
|
| 17 |
+
try:
|
| 18 |
+
from code.predictor import EnsemblePredictor # expensive import left for after the argument check
|
| 19 |
+
|
| 20 |
+
pdb_code, pdb_path = Type_PDB(pdb_code)
|
| 21 |
+
orig_list = parseList(orig, parse_amino_acid)
|
| 22 |
+
loc_list = parseList(loc, int)
|
| 23 |
+
mut_list = parseList(mut, parse_amino_acid)
|
| 24 |
+
|
| 25 |
+
if len(loc_list) != len(mut_list):
|
| 26 |
+
if len(mut_list) == 1:
|
| 27 |
+
mut_list *= len(loc_list)
|
| 28 |
+
else:
|
| 29 |
+
return "Error: Inconsistent multi-point mutant specification"
|
| 30 |
+
|
| 31 |
+
predictor = EnsemblePredictor(version=version)
|
| 32 |
+
assessment, prediction = predictor.predict_change(pdb_path, chain, orig_list, loc_list, mut_list)
|
| 33 |
+
assessment_str = {'+': 'solubilizing', 'N': 'neutral', '-': 'desolubilizing'}[assessment]
|
| 34 |
+
|
| 35 |
+
ORANGE = (255, 165, 0)
|
| 36 |
+
BLUESH = (100, 100, 255)
|
| 37 |
+
# ternary gradient orange-black-blue
|
| 38 |
+
color = tuple((o*max(0, 1-prediction*2) + b*max(0, (2*prediction)**2-1) for o, b in zip(ORANGE, BLUESH)))
|
| 39 |
+
return f"Predicted solubility change: {prediction:.3f} <span style='color: rgb{color}'>({assessment_str})</span>"
|
| 40 |
+
except Exception as e:
|
| 41 |
+
return f"Error: {str(e)}"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# --- Gradio Interface ---
|
| 45 |
+
with gr.Blocks(
|
| 46 |
+
title="SoluProtMut", css="""
|
| 47 |
+
.gradio-container {
|
| 48 |
+
max-width: 900px !important
|
| 49 |
+
} blockquote {
|
| 50 |
+
margin: 1em !important
|
| 51 |
+
}
|
| 52 |
+
""") as demo:
|
| 53 |
+
gr.Markdown("""
|
| 54 |
+
## SoluProtMut: prediction of a mutational effect on protein solubility
|
| 55 |
+
specify the mutation in the protein of interest:""")
|
| 56 |
+
|
| 57 |
+
with gr.Row():
|
| 58 |
+
pdb_code = gr.Textbox(label="PDB Code",
|
| 59 |
+
placeholder="1EER",
|
| 60 |
+
max_length=4 # 12 # new PDB identifier has a shape of: pdb_00001abc https://proteopedia.org/w/PDB_code
|
| 61 |
+
)
|
| 62 |
+
chain = gr.Textbox(label="Chain", value="A", max_length=1, max_lines=1, scale=0)
|
| 63 |
+
|
| 64 |
+
with gr.Row():
|
| 65 |
+
loc = gr.Textbox(label="Mutation position(s)", placeholder="48,150")
|
| 66 |
+
orig = gr.Textbox(label="Wild-type residue(s)", placeholder="F,R", scale=0)
|
| 67 |
+
mut = gr.Textbox(label="Mutant residue(s)", placeholder="D[,A]", scale=0)
|
| 68 |
+
|
| 69 |
+
# with gr.Row():
|
| 70 |
+
# verbose = gr.Checkbox(label="Verbose Output")
|
| 71 |
+
# version = gr.Textbox(label="Model Version (optional)", placeholder="v1.0")
|
| 72 |
+
|
| 73 |
+
output = gr.HTML()
|
| 74 |
+
|
| 75 |
+
predict_btn = gr.Button("Predict solubility effect", variant='primary', size='lg', scale=0)
|
| 76 |
+
# predict_btn.style(full_width=False)
|
| 77 |
+
predict_btn.click(fn=predict_solubility,
|
| 78 |
+
inputs=[pdb_code, chain, orig, loc, mut],
|
| 79 |
+
outputs=[output])
|
| 80 |
+
|
| 81 |
+
gr.Markdown(value="""
|
| 82 |
+
<br/>
|
| 83 |
+
|
| 84 |
+
**Acknowledgement**. Please, use the following citation to acknowledge the use of our tool:
|
| 85 |
+
> Velecký, J., Faldynová H., Hermosilla, P., Sandlerová, N., Dörr, M., Egersdorfová, S., Bornscheuer, U., Prokop, Z., Damborský, J., Mazurenko, S., 2025:
|
| 86 |
+
> SoluProtMut: Siamese Deep Learning for Solubility Effect Prediction in Protein Mutations and Experimental Validation.
|
| 87 |
+
> *In preparation.*
|
| 88 |
+
""")
|
| 89 |
+
|
| 90 |
+
demo.launch()
|
code/VHSE.csv
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
AA,VHSE1,VHSE2,VHSE3,VHSE4,VHSE5,VHSE6,VHSE7,VHSE8
|
| 2 |
+
A,0.15,-1.11,-1.35,-0.92,0.02,-0.91,0.36,-0.48
|
| 3 |
+
C,0.18,-1.67,-0.46,-0.21,0,1.2,-1.61,-0.19
|
| 4 |
+
D,-1.15,0.67,-0.41,-0.01,-2.68,1.31,0.03,0.56
|
| 5 |
+
E,-1.18,0.4,0.1,0.36,-2.16,-0.17,0.91,0.02
|
| 6 |
+
F,1.52,0.61,0.96,-0.16,0.25,0.28,-1.33,-0.2
|
| 7 |
+
G,-0.2,-1.53,-2.63,2.28,-0.53,-1.18,2.01,-1.34
|
| 8 |
+
H,-0.43,-0.25,0.37,0.19,0.51,1.28,0.93,0.65
|
| 9 |
+
I,1.27,-0.14,0.3,-1.8,0.3,-1.61,-0.16,-0.13
|
| 10 |
+
K,-1.17,0.7,0.7,0.8,1.64,0.67,1.63,0.13
|
| 11 |
+
L,1.36,0.07,0.36,-0.8,0.22,-1.37,0.08,-0.62
|
| 12 |
+
M,1.01,-0.53,0.43,0,0.23,0.1,-0.86,-0.68
|
| 13 |
+
N,-0.99,0,-0.37,0.69,-0.55,0.85,0.73,-0.8
|
| 14 |
+
P,0.22,-0.17,-0.5,0.05,-0.01,-1.34,-0.19,3.56
|
| 15 |
+
Q,-0.96,0.12,0.18,0.16,0.09,0.42,-0.2,-0.41
|
| 16 |
+
R,-1.47,1.45,1.24,1.27,1.55,1.47,1.3,0.83
|
| 17 |
+
S,-0.67,-0.86,-1.07,-0.41,-0.32,0.27,-0.64,0.11
|
| 18 |
+
T,-0.34,-0.51,-0.55,-1.06,-0.06,-0.01,-0.79,0.39
|
| 19 |
+
V,0.76,-0.92,-0.17,-1.91,0.22,-1.4,-0.24,-0.03
|
| 20 |
+
W,1.5,2.06,1.79,0.75,0.75,-0.13,-1.01,-0.85
|
| 21 |
+
Y,0.61,1.6,1.17,0.73,0.53,0.25,-0.96,-0.52
|
code/__init__.py
ADDED
|
File without changes
|
code/data_loader.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
from Bio.PDB.Polypeptide import one_to_index
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch.utils.data import Dataset
|
| 10 |
+
|
| 11 |
+
class Collate_Protein_Batch():
|
| 12 |
+
|
| 13 |
+
@staticmethod
|
| 14 |
+
def collate(p_batch):
|
| 15 |
+
batch_names = []
|
| 16 |
+
batch_aas = []
|
| 17 |
+
batch_coords = []
|
| 18 |
+
batch_seq_pos = []
|
| 19 |
+
batch_axes = []
|
| 20 |
+
batch_instance = []
|
| 21 |
+
batch_labels = []
|
| 22 |
+
batch_weights = []
|
| 23 |
+
|
| 24 |
+
cur_iter = 0
|
| 25 |
+
for protA, protB, label, w in p_batch:
|
| 26 |
+
for chain in (protA, protB):
|
| 27 |
+
if chain:
|
| 28 |
+
batch_names.append(chain[0])
|
| 29 |
+
batch_aas.append(chain[1])
|
| 30 |
+
batch_coords.append(chain[2])
|
| 31 |
+
batch_seq_pos.append(chain[3])
|
| 32 |
+
batch_axes.append(chain[4])
|
| 33 |
+
batch_instance.append(np.ones_like(chain[1])*cur_iter)
|
| 34 |
+
cur_iter += 1
|
| 35 |
+
batch_labels.append(label)
|
| 36 |
+
batch_weights.append(w)
|
| 37 |
+
|
| 38 |
+
batch_labels = list(filter(lambda l: l is not None, batch_labels))
|
| 39 |
+
|
| 40 |
+
return batch_names,\
|
| 41 |
+
torch.as_tensor(np.concatenate(batch_aas, axis=0)),\
|
| 42 |
+
torch.as_tensor(np.concatenate(batch_coords, axis=0)),\
|
| 43 |
+
torch.as_tensor(np.concatenate(batch_seq_pos, axis=0)),\
|
| 44 |
+
torch.as_tensor(np.concatenate(batch_axes, axis=0)),\
|
| 45 |
+
torch.as_tensor(np.concatenate(batch_instance, axis=0)).to(torch.int32),\
|
| 46 |
+
torch.as_tensor(batch_weights),\
|
| 47 |
+
torch.as_tensor(batch_labels)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# AA Letter to id
|
| 51 |
+
AA1 = "ACDEFGHIKLMNPQRSTVWYX"
|
| 52 |
+
AA_TO_ID = {}
|
| 53 |
+
for i in range(0, 21):
|
| 54 |
+
AA_TO_ID[AA1[i]] = i
|
| 55 |
+
|
| 56 |
+
def create_datapoint(pdb_code: str, seq: str, coords, w: float = 1):
|
| 57 |
+
return (
|
| 58 |
+
(
|
| 59 |
+
pdb_code,
|
| 60 |
+
[AA_TO_ID[aa] for aa in seq],
|
| 61 |
+
coords,
|
| 62 |
+
list(range(len(seq))),
|
| 63 |
+
[],
|
| 64 |
+
[]
|
| 65 |
+
), None, None, w
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def collate_batch(p_batch):
|
| 69 |
+
return Collate_Protein_Batch.collate(p_batch)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class EnzymeClassDataset(Dataset):
|
| 73 |
+
|
| 74 |
+
def __init__(
|
| 75 |
+
self,
|
| 76 |
+
p_path = 'data',
|
| 77 |
+
p_data_path = 'chains',
|
| 78 |
+
p_dataset = 'training',
|
| 79 |
+
p_fastafile = 'chain_list_pdb.fasta',
|
| 80 |
+
p_random_seed = None,
|
| 81 |
+
p_fold: str = None, # particular fold from 1 to N
|
| 82 |
+
p_train_mode = False, # to select all but the given fold (for training)
|
| 83 |
+
p_data_aug = False,
|
| 84 |
+
p_batch_pairs = False,
|
| 85 |
+
p_load_data = False
|
| 86 |
+
):
|
| 87 |
+
if p_fold is not None and int(p_fold) < 1:
|
| 88 |
+
raise Exception("Fold for CV should be a positive integer! Got: " + str(p_fold))
|
| 89 |
+
|
| 90 |
+
# Random state.
|
| 91 |
+
self.random_state_ = np.random.RandomState(p_random_seed)
|
| 92 |
+
|
| 93 |
+
# Save the data augmentation parameters.
|
| 94 |
+
self.data_augment_ = p_data_aug
|
| 95 |
+
self.batch_pairs_ = p_batch_pairs
|
| 96 |
+
|
| 97 |
+
# Get the paths.
|
| 98 |
+
self.pdb_folder_ = os.path.join(os.path.join(p_path, p_data_path))
|
| 99 |
+
pdb_fasta_file = os.path.join(p_path, p_fastafile)
|
| 100 |
+
|
| 101 |
+
# Load the sequences from the fasta file
|
| 102 |
+
self.list_chains_ = {}
|
| 103 |
+
def process_fasta_file(fasta_file, folder):
|
| 104 |
+
with open(fasta_file, 'r') as my_fasta_file:
|
| 105 |
+
chain_name = ''
|
| 106 |
+
for cur_line in my_fasta_file.readlines():
|
| 107 |
+
if cur_line.startswith('>'):
|
| 108 |
+
chain_name = cur_line.rstrip()[1:]
|
| 109 |
+
else:
|
| 110 |
+
cur_chain = cur_line.rstrip()
|
| 111 |
+
cur_chain_ids = []
|
| 112 |
+
for cur_aa in cur_chain:
|
| 113 |
+
cur_chain_ids.append(AA_TO_ID[cur_aa])
|
| 114 |
+
self.list_chains_[chain_name] = (np.array(cur_chain_ids), folder)
|
| 115 |
+
|
| 116 |
+
process_fasta_file(pdb_fasta_file, self.pdb_folder_)
|
| 117 |
+
|
| 118 |
+
# load datapoints
|
| 119 |
+
self.datapoints_ = []
|
| 120 |
+
with open(os.path.join(p_path, p_dataset+'.csv'), 'r') as labels_map_file:
|
| 121 |
+
for cur_line in labels_map_file:
|
| 122 |
+
line_split = cur_line.rstrip().split(',')
|
| 123 |
+
line_split[2] = float(line_split[2])
|
| 124 |
+
line_split[3] = float(line_split[3]) if line_split[3] else 1 # set default weight if not available
|
| 125 |
+
# Cross-validation row selection
|
| 126 |
+
if p_fold and (line_split[4] == p_fold) == p_train_mode:
|
| 127 |
+
continue # do not include this fold
|
| 128 |
+
self.datapoints_.append(line_split[:4]) # orig_pdb, mut_pdb, label, weight
|
| 129 |
+
|
| 130 |
+
if p_load_data:
|
| 131 |
+
self.data_ = []
|
| 132 |
+
print()
|
| 133 |
+
for cur_iter, cur_chain in enumerate(self.list_chains_):
|
| 134 |
+
cur_path = os.path.join(cur_chain[2], cur_chain[0]+".npy")
|
| 135 |
+
cur_pos_seq_path = os.path.join(cur_chain[2], cur_chain[0]+"_seq_pos.npy")
|
| 136 |
+
# cur_axes_path = os.path.join(cur_chain[2], cur_chain[0]+"_axes.npy")
|
| 137 |
+
cur_aces_path = []
|
| 138 |
+
self.data_.append((np.load(cur_path), np.load(cur_pos_seq_path), np.load(cur_axes_path)))
|
| 139 |
+
if cur_iter%100==0:
|
| 140 |
+
print("\r Loading {:6d}/{:6d}".format(cur_iter, len(self.list_chains_)), end ="")
|
| 141 |
+
print()
|
| 142 |
+
else:
|
| 143 |
+
self.data_ = None
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def __len__(self):
|
| 147 |
+
return len(self.datapoints_)
|
| 148 |
+
|
| 149 |
+
def __getitem__(self, idx):
|
| 150 |
+
orig_pdb, mut_pdb, label, weight = self.datapoints_[idx]
|
| 151 |
+
|
| 152 |
+
orig_path = os.path.join(self.list_chains_[orig_pdb][1], orig_pdb +".npy")
|
| 153 |
+
mut_path = os.path.join(self.list_chains_[mut_pdb][1], mut_pdb + ".npy")
|
| 154 |
+
# cur_pos_seq_path = os.path.join(self.list_chains_[idx][2], self.list_chains_[idx][0]+"_seq_pos.npy")
|
| 155 |
+
# cur_axes_path = os.path.join(self.list_chains_[idx][2], self.list_chains_[idx][0]+"_axes.npy")
|
| 156 |
+
cur_axes_path = []
|
| 157 |
+
|
| 158 |
+
noise = None
|
| 159 |
+
|
| 160 |
+
def get_pdb(idx, cur_path, label: int):
|
| 161 |
+
nonlocal noise
|
| 162 |
+
|
| 163 |
+
cur_aa_ids = self.list_chains_[idx][0]
|
| 164 |
+
if self.data_ is None:
|
| 165 |
+
cur_pos = np.load(cur_path)
|
| 166 |
+
# cur_seq_pos = np.load(cur_pos_seq_path)
|
| 167 |
+
cur_seq_pos = list(range(len(cur_aa_ids)))
|
| 168 |
+
cur_axes = []
|
| 169 |
+
else:
|
| 170 |
+
cur_pos = self.data_[idx][0]
|
| 171 |
+
cur_seq_pos = self.data_[idx][1]
|
| 172 |
+
cur_axes = self.data_[idx][2]
|
| 173 |
+
|
| 174 |
+
cur_min = np.amin(cur_pos, axis=0, keepdims=True)
|
| 175 |
+
cur_max = np.amax(cur_pos, axis=0, keepdims=True)
|
| 176 |
+
center = (cur_max + cur_min)*0.5
|
| 177 |
+
cur_pos = cur_pos - center
|
| 178 |
+
|
| 179 |
+
if self.data_augment_:
|
| 180 |
+
if noise is None or not self.batch_pairs_:
|
| 181 |
+
noise = self.random_state_.normal(0.0, 0.05, cur_pos.shape)
|
| 182 |
+
|
| 183 |
+
assert cur_pos.shape == noise.shape
|
| 184 |
+
# print(cur_pos)
|
| 185 |
+
cur_pos = cur_pos + noise
|
| 186 |
+
# print(cur_pos)
|
| 187 |
+
|
| 188 |
+
return idx, cur_aa_ids, cur_pos, cur_seq_pos, cur_axes
|
| 189 |
+
|
| 190 |
+
return get_pdb(orig_pdb, orig_path, label), get_pdb(mut_pdb, mut_path, label), label, weight
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
|
code/data_preprocessing/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TMPDIR = "tmp/"
|
| 2 |
+
|
| 3 |
+
class PreprocessError(Exception):
|
| 4 |
+
pass
|
| 5 |
+
|
| 6 |
+
from .process import process_pdb, mutate_seq
|
| 7 |
+
from .model import FoldX, get_PDB
|
code/data_preprocessing/model.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os.path, sys
|
| 2 |
+
|
| 3 |
+
from . import TMPDIR, PreprocessError
|
| 4 |
+
os.makedirs(TMPDIR, exist_ok=True)
|
| 5 |
+
|
| 6 |
+
FILEDIR = os.path.dirname(__file__)
|
| 7 |
+
FOLDX_DIR = FILEDIR + "/../../external/foldx/"
|
| 8 |
+
FOLDX_BINARY = FOLDX_DIR + "foldx"
|
| 9 |
+
# if sys.platform == "win32":
|
| 10 |
+
# FOLDX_BINARY = FOLDX_DIR + "foldx-32"
|
| 11 |
+
FOLDX_ROTAFILE = os.path.relpath(FOLDX_DIR) + "/rotabase.txt"
|
| 12 |
+
# os.environ["FOLDX_BINARY"] = os.path.dirname(__file__) + "/external/foldx/foldx-32.exe"
|
| 13 |
+
|
| 14 |
+
# verbosity of logs
|
| 15 |
+
VERBOSE_IMPORTANT = 0
|
| 16 |
+
VERBOSE_VERBOSE = 1
|
| 17 |
+
VERBOSE_LEVEL = VERBOSE_IMPORTANT
|
| 18 |
+
|
| 19 |
+
def echo(txt="", level=VERBOSE_VERBOSE):
|
| 20 |
+
if(level <= VERBOSE_LEVEL):
|
| 21 |
+
print(txt)
|
| 22 |
+
|
| 23 |
+
def echoStage(name, exe = None):
|
| 24 |
+
echo()
|
| 25 |
+
name = " %s " % name
|
| 26 |
+
TEXT = "predictor:----------------------------------------------------------------------"
|
| 27 |
+
hyphen_len = int((len(TEXT) - len(name)) / 2)
|
| 28 |
+
TEXT = TEXT[0:hyphen_len] + name + TEXT[-hyphen_len:]
|
| 29 |
+
echo(TEXT)
|
| 30 |
+
if exe:
|
| 31 |
+
echo("Running: %s" % exe)
|
| 32 |
+
else:
|
| 33 |
+
echo()
|
| 34 |
+
|
| 35 |
+
def get_PDB(pdb_code: str, force: bool = False):
|
| 36 |
+
# todo: make filename small
|
| 37 |
+
from urllib import request, error
|
| 38 |
+
global TMPDIR
|
| 39 |
+
|
| 40 |
+
pdb_filename = pdb_code + ".pdb"
|
| 41 |
+
pdb_filepath = TMPDIR + pdb_filename
|
| 42 |
+
if force or not os.path.isfile(pdb_filepath):
|
| 43 |
+
try:
|
| 44 |
+
request.urlretrieve('https://files.rcsb.org/download/' + pdb_filename, pdb_filepath)
|
| 45 |
+
except error.HTTPError as e:
|
| 46 |
+
if e.code == 404:
|
| 47 |
+
raise PreprocessError("PDB file not available online – '%s': %s" % (pdb_code, e.url))
|
| 48 |
+
raise
|
| 49 |
+
|
| 50 |
+
return pdb_filepath
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def exec(cmd, level=VERBOSE_VERBOSE, **args):
|
| 54 |
+
import subprocess
|
| 55 |
+
print(cmd)
|
| 56 |
+
|
| 57 |
+
pipe = None if level <= VERBOSE_LEVEL else subprocess.DEVNULL
|
| 58 |
+
return subprocess.call(cmd, shell=True, stdout=pipe, **args)
|
| 59 |
+
|
| 60 |
+
def FoldX(file,
|
| 61 |
+
chain: str = None,
|
| 62 |
+
wildtype: list = None,
|
| 63 |
+
location: list = None,
|
| 64 |
+
mutation: list = None,
|
| 65 |
+
force: bool=False
|
| 66 |
+
):
|
| 67 |
+
global TMPDIR, FOLDX_BINARY, FOLDX_ROTAFILE
|
| 68 |
+
|
| 69 |
+
if(exec("\"%s\" -h" % FOLDX_BINARY)):
|
| 70 |
+
raise PreprocessError("FoldX is not installed or has expired!")
|
| 71 |
+
|
| 72 |
+
dname = os.path.dirname(file) or '.'
|
| 73 |
+
fname = os.path.basename(file)
|
| 74 |
+
|
| 75 |
+
# prepare PDB structure for run in FoldX
|
| 76 |
+
rname = "_Repair".join(os.path.splitext(fname)) # repaired PDB filename
|
| 77 |
+
rpath = TMPDIR + rname
|
| 78 |
+
if force or not os.path.isfile(rpath):
|
| 79 |
+
print(TMPDIR)
|
| 80 |
+
exe = "\"%s\" --command=RepairPDB --pdb-dir=\"%s\" --pdb=\"%s\" --rotabaseLocation=\"%s\" --output-dir=\"%s\"" % \
|
| 81 |
+
(FOLDX_BINARY, dname, fname, FOLDX_ROTAFILE, TMPDIR)
|
| 82 |
+
echoStage("EXECUTING FOLDX AS MUTATION ENGINE", exe)
|
| 83 |
+
# Repair PDB
|
| 84 |
+
exit_code = exec(exe)
|
| 85 |
+
if exit_code:
|
| 86 |
+
raise PreprocessError("FoldX optimization failed with code: %i" % (exit_code))
|
| 87 |
+
return None, None
|
| 88 |
+
|
| 89 |
+
if not mutation:
|
| 90 |
+
return rpath, None
|
| 91 |
+
|
| 92 |
+
# prepare mutation list for FoldX
|
| 93 |
+
ifile = TMPDIR + 'individual_list.txt'
|
| 94 |
+
muts = []
|
| 95 |
+
for w,l,m in zip(wildtype, location, mutation):
|
| 96 |
+
muts.append("%s%s%s%s" % (w,chain,l,m))
|
| 97 |
+
with open(ifile, 'w') as individuals:
|
| 98 |
+
individuals.write(','.join(muts) + ';')
|
| 99 |
+
|
| 100 |
+
# Create mutants
|
| 101 |
+
# --numberOfRuns=5 does not seems to change structure a lot, does not influence the backbone
|
| 102 |
+
exe = "\"%s\" --command=BuildModel --pdb-dir=\"%s\" --pdb=\"%s\" --rotabaseLocation=\"%s\" --output-dir=\"%s\" --mutant-file=\"%s\"" % \
|
| 103 |
+
(FOLDX_BINARY, TMPDIR, rname, FOLDX_ROTAFILE, TMPDIR, ifile)
|
| 104 |
+
exit_code = exec(exe)
|
| 105 |
+
# exit_code = 0
|
| 106 |
+
if exit_code:
|
| 107 |
+
raise PreprocessError("FoldX mutation failed with code: %i" % (exit_code))
|
| 108 |
+
return rpath, None
|
| 109 |
+
|
| 110 |
+
mname = TMPDIR + "_1".join(os.path.splitext(rname)) # 1st mutant of inidivudals file has "_Repair_1" suffix
|
| 111 |
+
|
| 112 |
+
# name of optimized original file, name of mutated file
|
| 113 |
+
return rpath, mname
|
code/data_preprocessing/process.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# author: Jan Velecky, adapted from Pedro
|
| 2 |
+
import os
|
| 3 |
+
import numpy as np
|
| 4 |
+
import gzip
|
| 5 |
+
import pickle
|
| 6 |
+
|
| 7 |
+
ROOT_DIR = os.path.dirname(__file__) + '/../../'
|
| 8 |
+
RAW_DIR = ROOT_DIR + '/data_raw/pdbs/'
|
| 9 |
+
OPTIMS_DIR = ROOT_DIR + '/data_temp/optimized-pdbs/'
|
| 10 |
+
MUTANTS_DIR = ROOT_DIR + '/data_temp/mutated-pdbs/'
|
| 11 |
+
|
| 12 |
+
PREPRO_DIR = ROOT_DIR + '/data_preprocessed/'
|
| 13 |
+
CHAINS_DIR = PREPRO_DIR + '/chains/'
|
| 14 |
+
|
| 15 |
+
# should not contain multi-chains
|
| 16 |
+
# should deal with partial residues
|
| 17 |
+
# mutated residues should be in the structure
|
| 18 |
+
|
| 19 |
+
import warnings
|
| 20 |
+
warnings.filterwarnings("ignore")
|
| 21 |
+
|
| 22 |
+
from Bio.PDB.PDBParser import PDBParser
|
| 23 |
+
from Bio.PDB.Polypeptide import is_aa, three_to_one
|
| 24 |
+
|
| 25 |
+
parser = PDBParser(PERMISSIVE=1)
|
| 26 |
+
|
| 27 |
+
# a dict of {pos: aa} with some str compability
|
| 28 |
+
class Type_Seq(dict):
|
| 29 |
+
def __init__(self, *arg, **kw):
|
| 30 |
+
super().__init__(*arg, **kw)
|
| 31 |
+
|
| 32 |
+
def __str__(self): # directly convertible to amino-acid sequence string
|
| 33 |
+
return ''.join(self.values())
|
| 34 |
+
|
| 35 |
+
def __repr__(self):
|
| 36 |
+
return str(dict(self))
|
| 37 |
+
|
| 38 |
+
def __iter__(self): # dict default is over the keys
|
| 39 |
+
yield from self.values()
|
| 40 |
+
|
| 41 |
+
# def __getitem__(self, subscript): # slicing would be nice to have, but should it slice by indexes or as sequence? Who knows.
|
| 42 |
+
# if isinstance(subscript, slice):
|
| 43 |
+
# return self.items() print(subscript.start, subscript.stop, subscript.step)
|
| 44 |
+
# else:
|
| 45 |
+
# return dict[subscript]
|
| 46 |
+
|
| 47 |
+
def copy(self): # sing super's (dict's) copy would create another dict, not a Type_Seq
|
| 48 |
+
return Type_Seq(dict(self))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _process_pdb(filepath, gap_detect=True):
|
| 52 |
+
structure_name = filepath.split('/')[-1][:-4]
|
| 53 |
+
|
| 54 |
+
with open(filepath, 'rt') as ifh:
|
| 55 |
+
structure = parser.get_structure(structure_name, ifh)
|
| 56 |
+
|
| 57 |
+
for chain in structure[0]:
|
| 58 |
+
res_aas = {} # sequence from the pdb
|
| 59 |
+
res_pos = [] # positions of Cα
|
| 60 |
+
seqid1 = None
|
| 61 |
+
for residue in chain:
|
| 62 |
+
if is_aa(residue.get_resname()):
|
| 63 |
+
if 'CA' in residue:
|
| 64 |
+
_, seqid, _ = residue.id
|
| 65 |
+
if(gap_detect):
|
| 66 |
+
if seqid1:
|
| 67 |
+
gap = seqid - seqid1
|
| 68 |
+
if gap > 8:
|
| 69 |
+
print("Gap of %i AAs detected at %s" % (gap, str(residue.full_id)))
|
| 70 |
+
seqid1 = seqid
|
| 71 |
+
|
| 72 |
+
try: # std amino acids
|
| 73 |
+
res_aas[seqid] = three_to_one(residue.get_resname())
|
| 74 |
+
except KeyError as e:
|
| 75 |
+
res_aas[seqid] = 'X'
|
| 76 |
+
atom_ca = residue['CA']
|
| 77 |
+
res_pos.append(atom_ca.get_coord())
|
| 78 |
+
|
| 79 |
+
res_pos = np.array(res_pos)
|
| 80 |
+
|
| 81 |
+
yield chain.id, Type_Seq(res_aas), res_pos
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def process_pdb(filepath, chain: str = None):
|
| 85 |
+
chains = _process_pdb(filepath)
|
| 86 |
+
if not chain: # return generator (for all chains)
|
| 87 |
+
return chains
|
| 88 |
+
|
| 89 |
+
for ch in chains: # return the specified chain
|
| 90 |
+
if ch[0] == chain:
|
| 91 |
+
return ch[1:]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def mutate_seq(res_aas: Type_Seq, wildtype: list, location: list, mutation: list) -> Type_Seq:
|
| 95 |
+
res_aas = res_aas.copy()
|
| 96 |
+
|
| 97 |
+
for w,l,m in zip(wildtype, location, mutation):
|
| 98 |
+
if res_aas[l] != w:
|
| 99 |
+
raise ValueError("Wildtype residue mismatch: %s is actually %s" % (''.join((w,str(l),m)), res_aas[l]))
|
| 100 |
+
res_aas[l] = m
|
| 101 |
+
|
| 102 |
+
return res_aas
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
|
| 107 |
+
list_pdbs = []
|
| 108 |
+
set_pdbs = set()
|
| 109 |
+
# process all PDBs
|
| 110 |
+
for pdb_path in [OPTIMS_DIR, RAW_DIR, MUTANTS_DIR]:
|
| 111 |
+
# use RAW structure if optimized not found
|
| 112 |
+
pdbs = {f for f in os.listdir(pdb_path) if os.path.isfile(os.path.join(pdb_path, f))}
|
| 113 |
+
pdbs -= set_pdbs
|
| 114 |
+
set_pdbs |= pdbs # merge sets
|
| 115 |
+
list_pdbs += [os.path.join(pdb_path, f) for f in pdbs]
|
| 116 |
+
|
| 117 |
+
total_chains = 0
|
| 118 |
+
os.makedirs(CHAINS_DIR, exist_ok=True) # folders for preprocessed data
|
| 119 |
+
|
| 120 |
+
with open(PREPRO_DIR+"chain_list_pdb.fasta", 'w') as chain_list_file:
|
| 121 |
+
for cur_iter, cur_pdb in enumerate(list_pdbs):
|
| 122 |
+
structure_name = cur_pdb.split('/')[-1][:-4].lower()
|
| 123 |
+
|
| 124 |
+
if cur_iter% 100 == 0:
|
| 125 |
+
print("%i/%i %s" % (cur_iter, len(list_pdbs), structure_name))
|
| 126 |
+
|
| 127 |
+
for chain_id, seq, res_pos in process_pdb(cur_pdb): # todo: _process_pdb(cur_pdb, gap_detect=False)
|
| 128 |
+
if len(seq) > 10:
|
| 129 |
+
total_chains += 1
|
| 130 |
+
|
| 131 |
+
chain_list_file.write(">"+structure_name+"."+chain_id+"\n")
|
| 132 |
+
chain_list_file.write(seq+"\n")
|
| 133 |
+
|
| 134 |
+
np.save(CHAINS_DIR+structure_name+"."+chain_id, res_pos)
|
| 135 |
+
|
| 136 |
+
print("Total chains:", total_chains)
|
code/model_py/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .v21_2 import PlaNNet, MutPred, Ensemble, conf_dict, feed
|
| 2 |
+
|
| 3 |
+
# Print number of parameters.
|
| 4 |
+
def get_n_params(model):
|
| 5 |
+
pp=0
|
| 6 |
+
for p in model.parameters():
|
| 7 |
+
nn=1
|
| 8 |
+
for s in list(p.size()):
|
| 9 |
+
nn = nn*s
|
| 10 |
+
pp += nn
|
| 11 |
+
return pp
|
| 12 |
+
|
| 13 |
+
def print_n_params(model, out_f = print):
|
| 14 |
+
out_f("Number of parameters: ", get_n_params(model))
|
code/model_py/utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from time import time
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
_current_milli_time = lambda: time() * 1000.0
|
| 6 |
+
|
| 7 |
+
# feeds batch to the predictor
|
| 8 |
+
def feed(model, cur_batch):
|
| 9 |
+
start_time = _current_milli_time()
|
| 10 |
+
chain_name, chain, chain_pos, chain_seq_pos, chain_axes, batch_ids = cur_batch
|
| 11 |
+
|
| 12 |
+
dev = next(model.parameters()).device
|
| 13 |
+
# move the batch to the same device as model is in
|
| 14 |
+
chain = chain.to(dev, torch.int64)
|
| 15 |
+
chain_pos = chain_pos.to(dev, torch.float32)
|
| 16 |
+
chain_seq_pos = chain_seq_pos.to(dev, torch.int64)
|
| 17 |
+
chain_axes = chain_axes.to(dev, torch.float32)
|
| 18 |
+
batch_ids = batch_ids.to(dev)
|
| 19 |
+
|
| 20 |
+
time_data = _current_milli_time() - start_time
|
| 21 |
+
|
| 22 |
+
logits = model( # [batch_size] of residues each:
|
| 23 |
+
chain, # AA type (index)
|
| 24 |
+
chain_pos, # coordinates in 3D (of a-Carbon?)
|
| 25 |
+
chain_seq_pos, # seqential index
|
| 26 |
+
chain_axes, # ??? axes (orthonormal) between consecutive residues
|
| 27 |
+
batch_ids # chain ID mask
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
return logits, time_data
|
| 31 |
+
|
| 32 |
+
class Feeder:
|
| 33 |
+
# enables feeding models with batches in objective way: model.feed(batch) instead of imperative feed(model, batch)
|
| 34 |
+
# simplifies imports...
|
| 35 |
+
def feed(self, *args):
|
| 36 |
+
return feed(self, *args)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class _Ensemble(torch.nn.Module, Feeder):
|
| 40 |
+
|
| 41 |
+
def __init__(self,
|
| 42 |
+
paths_or_n,
|
| 43 |
+
base_nn: torch.nn.Module,
|
| 44 |
+
mut_nn: torch.nn.Module,
|
| 45 |
+
):
|
| 46 |
+
super().__init__()
|
| 47 |
+
models = []
|
| 48 |
+
|
| 49 |
+
if type(paths_or_n) is int:
|
| 50 |
+
for i in range(paths_or_n):
|
| 51 |
+
models.append(mut_nn(base_nn('avg', True, False))) # todo: this should not be hardcoded
|
| 52 |
+
else:
|
| 53 |
+
for path in paths_or_n:
|
| 54 |
+
conf_dict = torch.load(path)
|
| 55 |
+
model = mut_nn(
|
| 56 |
+
base_nn(conf_dict['gl_pooling'], bool(conf_dict['embed_aa']), conf_dict['embed_aa'] == 'learn'))
|
| 57 |
+
model.load_state_dict(conf_dict["state_dict_prot_enc"])
|
| 58 |
+
model.eval()
|
| 59 |
+
models.append(model)
|
| 60 |
+
# all modules has to be properly registered as modules to be visible for PyTorch
|
| 61 |
+
self.models = torch.nn.ModuleList(
|
| 62 |
+
models) # jit.trace would not work without this with a crpytic message: RuntimeError: "Cannot insert a Tensor that requires grad as a constant"
|
| 63 |
+
|
| 64 |
+
def forward(self, *args):
|
| 65 |
+
preds = [m(*args) for m in self.models]
|
| 66 |
+
preds = torch.cat(preds)
|
| 67 |
+
return torch.mean(preds)
|
code/model_py/v21_2.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import signal
|
| 2 |
+
|
| 3 |
+
# RUN CONFIGURATION
|
| 4 |
+
VERSION = "21.2-mutpred"
|
| 5 |
+
VERSION_DESC = "VERSION_DESC..." # DEPRECATED?
|
| 6 |
+
conf_dict = {
|
| 7 |
+
# model configuration
|
| 8 |
+
'embed_aa': True, # True (VHSE) | False (1-Hot) | 'learn'
|
| 9 |
+
'gl_pool': 'avg', # both|avg
|
| 10 |
+
'L1_features': 128, # e.g.: 128,256,...
|
| 11 |
+
'cl_features': 1024, # classifier hidden neurons count
|
| 12 |
+
'conv_features': 8, # convolution features (32 in the IEConv paper)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 18 |
+
|
| 19 |
+
from torch_geometric.nn import radius_graph as ball_query
|
| 20 |
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool, InstanceNorm, BatchNorm
|
| 21 |
+
from torch_geometric.nn.conv import GCNConv
|
| 22 |
+
from torch_geometric.nn.pool import avg_pool_x
|
| 23 |
+
from torch_geometric.data import Data
|
| 24 |
+
from torch_geometric.transforms import Distance
|
| 25 |
+
from torch.nn.functional import one_hot
|
| 26 |
+
from torch.nn import Embedding, Linear, Sequential, ReLU, Sigmoid
|
| 27 |
+
from torch.nn import Dropout3d as Dropout # Dropout2d, Dropdout, 3d and Dropout1d are calling the same function underneath (the last one available since PyTorch 1.12)
|
| 28 |
+
from torch_scatter import scatter
|
| 29 |
+
from sklearn.metrics import balanced_accuracy_score as BA_score
|
| 30 |
+
|
| 31 |
+
from torch.utils.data import DataLoader
|
| 32 |
+
|
| 33 |
+
from .utils import feed, Feeder, _Ensemble # feed for backward compatibility (import from this module)
|
| 34 |
+
|
| 35 |
+
EC_CLASSES = 1 # 1 (2) class or regression
|
| 36 |
+
AA_CLASSES = 21 # 20 standard AAs + X
|
| 37 |
+
VHSE_DIM = 8 # dimension count of VHSE embedding
|
| 38 |
+
CONV_HIDDENS = conf_dict['conv_features']
|
| 39 |
+
MAX_HOPS = 6
|
| 40 |
+
DROPOUT_RATE = 0 # 0.2
|
| 41 |
+
DROPOUT_CL_RATE = 0.5
|
| 42 |
+
|
| 43 |
+
# some PyTorch Geometric function does not respect batch_mask
|
| 44 |
+
def batch_clusters(cluster, batch_mask, safe_margin=2):
|
| 45 |
+
return cluster + (int(cluster.max()) + 8) // 8 * 8 * batch_mask
|
| 46 |
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 47 |
+
# pseudocode: (cluster.max() + 8) >> 3 << 3
|
| 48 |
+
# each offset is a multiply of cluster.max()+1 rounded up to 8s in binary (0..7 --> 8, 8..15 -> 16, etc.)
|
| 49 |
+
# note: retyping to int() to get rid of an irrelevant PyTorch 1 warning: UserWarning: __floordiv__ is deprecated...
|
| 50 |
+
# this is not good, because it would require to move data back and forth between CPU and GPU
|
| 51 |
+
# mask would depend on the largest protein
|
| 52 |
+
|
| 53 |
+
class BatchAwareDropout(torch.nn.Module):
|
| 54 |
+
r"""A placeholder identity operator that is argument-insensitive.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
args: any argument (unused)
|
| 58 |
+
kwargs: any keyword argument (unused)
|
| 59 |
+
|
| 60 |
+
Shape:
|
| 61 |
+
- Input: :math:`(*)`, where :math:`*` means any number of dimensions.
|
| 62 |
+
- Output: :math:`(*)`, same shape as the input.
|
| 63 |
+
|
| 64 |
+
Examples::
|
| 65 |
+
|
| 66 |
+
>>> m = nn.Identity(54, unused_argument1=0.1, unused_argument2=False)
|
| 67 |
+
>>> input = torch.randn(128, 20)
|
| 68 |
+
>>> output = m(input)
|
| 69 |
+
>>> print(output.size())
|
| 70 |
+
torch.Size([128, 20])
|
| 71 |
+
|
| 72 |
+
"""
|
| 73 |
+
def __init__(self, p: float = 0.5) -> None:
|
| 74 |
+
super().__init__()
|
| 75 |
+
self.p = p
|
| 76 |
+
|
| 77 |
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
| 78 |
+
if(self.p and self.training):
|
| 79 |
+
feature_shape = input[0].shape
|
| 80 |
+
mask = (
|
| 81 |
+
input.new_empty(feature_shape).uniform_() # tensor with uniformly distributed random numbers on the same device as input and dimensions as one data instance
|
| 82 |
+
> self.p # thresholding by dropout rate
|
| 83 |
+
).float().unsqueeze(0) / (1 - self.p) # normalization to keep similar "weight sum"
|
| 84 |
+
|
| 85 |
+
input = input.mul(mask)
|
| 86 |
+
return input
|
| 87 |
+
Dropout = BatchAwareDropout
|
| 88 |
+
|
| 89 |
+
class Print(torch.nn.Module):
|
| 90 |
+
def __init__(self):
|
| 91 |
+
super(Print, self).__init__()
|
| 92 |
+
|
| 93 |
+
def forward(self, x):
|
| 94 |
+
# print(x[0:20])
|
| 95 |
+
return x
|
| 96 |
+
|
| 97 |
+
# Double Layer Perceptron
|
| 98 |
+
# with droupouts + batch norm. and ReLU for the hidden layer
|
| 99 |
+
class DLP(torch.nn.Module):
|
| 100 |
+
def __init__(self, inputs, hiddens, outputs=1): # number of the input, hidden and ouput neurons
|
| 101 |
+
super().__init__()
|
| 102 |
+
|
| 103 |
+
self.hid = Sequential(
|
| 104 |
+
Dropout(DROPOUT_CL_RATE),
|
| 105 |
+
Linear(inputs, hiddens),
|
| 106 |
+
BatchNorm(hiddens),
|
| 107 |
+
ReLU()
|
| 108 |
+
)
|
| 109 |
+
self.out = Sequential(
|
| 110 |
+
Dropout(DROPOUT_CL_RATE),
|
| 111 |
+
Linear(hiddens, outputs)
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
def forward(self, x):
|
| 115 |
+
# batch norm
|
| 116 |
+
# dropout 0.5
|
| 117 |
+
# relu
|
| 118 |
+
# hidden layer
|
| 119 |
+
x = self.hid(x)
|
| 120 |
+
# batch norm
|
| 121 |
+
# dropout 0.5
|
| 122 |
+
# output layer
|
| 123 |
+
x = self.out(x)
|
| 124 |
+
return x
|
| 125 |
+
|
| 126 |
+
# Intrinsci-extrinsic convolution layer
|
| 127 |
+
class IEConv(torch.nn.Module):
|
| 128 |
+
def __init__(self, inputs, outputs, distance):
|
| 129 |
+
super().__init__()
|
| 130 |
+
|
| 131 |
+
self.distance = distance
|
| 132 |
+
self.inputs = inputs # input features
|
| 133 |
+
self.outputs = outputs # output features
|
| 134 |
+
|
| 135 |
+
self.intr_dist = Distance(max_value = MAX_HOPS)
|
| 136 |
+
self.extr_dist = Distance(max_value = self.distance)
|
| 137 |
+
|
| 138 |
+
self.slp1 = Sequential(
|
| 139 |
+
Linear(2, CONV_HIDDENS), # 2 types of distance
|
| 140 |
+
ReLU()
|
| 141 |
+
)
|
| 142 |
+
self.slp2 = Sequential(
|
| 143 |
+
# Dropout(DROPOUT_CL_RATE),
|
| 144 |
+
Linear(CONV_HIDDENS*inputs, outputs) # largest gradient matrix: [8,I]
|
| 145 |
+
)
|
| 146 |
+
# effectively implements the following but more frugally in terms of gradient (intermediate) tensor size (8*I << I*O):
|
| 147 |
+
# self.gcl = Sequential(
|
| 148 |
+
# DLP(2, 8, inputs*outputs), # [8,I*O] matrix size
|
| 149 |
+
# # ReLU()
|
| 150 |
+
# )
|
| 151 |
+
|
| 152 |
+
self.norm = BatchNorm(outputs)
|
| 153 |
+
|
| 154 |
+
def forward(self,
|
| 155 |
+
graphs: Data, # AAs connected to neighbouring AAs, position: sequential, node features
|
| 156 |
+
coords, # 3D cartesian coordinates
|
| 157 |
+
):
|
| 158 |
+
neighbors = graphs.edge_index
|
| 159 |
+
|
| 160 |
+
# 1st edge feature = intrinsic distance (along bonds)
|
| 161 |
+
graphs = self.intr_dist(graphs) # max_value is used just for nomalization in the step above
|
| 162 |
+
graphs.edge_attr = graphs.edge_attr.clamp(max=1.0) # get values into interval <0,1> for numerical stability
|
| 163 |
+
# NOTE: this way, information about long bond distance is lost (longer than MAX_HOPS)
|
| 164 |
+
|
| 165 |
+
# 2nd edge feature = extrinsic distance (euclidean)
|
| 166 |
+
graphs.pos = coords
|
| 167 |
+
graphs = self.extr_dist(graphs)
|
| 168 |
+
|
| 169 |
+
# batch norm, dropout 0.2, relu
|
| 170 |
+
|
| 171 |
+
# get weights from the convolution kernel
|
| 172 |
+
w = self.slp1(graphs.edge_attr) # (|edges|, 8)
|
| 173 |
+
w = torch.reshape(w, (-1, 1, CONV_HIDDENS)) # (|edges|, 1, 8)
|
| 174 |
+
# get input features and project them on the edges
|
| 175 |
+
h = graphs.x[neighbors[0]] # (|edges|, input_features)
|
| 176 |
+
h = torch.reshape(h, (-1, self.inputs, 1)) # (|edges|, input_features, 1)
|
| 177 |
+
# widen weights
|
| 178 |
+
h = w*h#torch.matmul(w, h) # (|edges|, 8, input_features)
|
| 179 |
+
h = torch.reshape(h, (-1, CONV_HIDDENS*self.inputs)) # (|edges|, 8*input_features)
|
| 180 |
+
assert_test(h)
|
| 181 |
+
# compute the new features factors (per edge)
|
| 182 |
+
# print(h)
|
| 183 |
+
h = self.slp2(h) # (|edges|, output_features)
|
| 184 |
+
assert_test(h)
|
| 185 |
+
# np.savetxt('h_before_scattered.txt', h.detach().cpu().numpy())
|
| 186 |
+
# finish convolution (sum vertex-wise the new features projected on the edges)
|
| 187 |
+
h = scatter(h, neighbors[1], dim=0, dim_size = graphs.num_nodes, reduce='add') # dim_size required - solitary AA may be in PDB (at the end of the sequence)
|
| 188 |
+
# print(h.shape)
|
| 189 |
+
# np.savetxt('h_scattered.txt', h.detach().cpu().numpy())
|
| 190 |
+
assert_test(h)
|
| 191 |
+
h = self.norm(h)
|
| 192 |
+
h = h.relu()
|
| 193 |
+
|
| 194 |
+
return h
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# like IEConv but employing ResNets
|
| 199 |
+
class ResNet(torch.nn.Module):
|
| 200 |
+
def __init__(self, inputs, outputs, distance):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.distance = distance
|
| 203 |
+
|
| 204 |
+
self.ldown = self.SLP(inputs, inputs//4)
|
| 205 |
+
self.conv = IEConv(inputs//4, inputs, distance)
|
| 206 |
+
self.lup = self.SLP(inputs, outputs)
|
| 207 |
+
|
| 208 |
+
self.lside = self.SLP(inputs, outputs) # side channel for passing the features of the node itself
|
| 209 |
+
|
| 210 |
+
def forward(self, graph, coords):
|
| 211 |
+
h = graph.x
|
| 212 |
+
|
| 213 |
+
graph.x = self.ldown(h)
|
| 214 |
+
x = self.conv(graph, coords)
|
| 215 |
+
x = self.lup(x)
|
| 216 |
+
|
| 217 |
+
h = self.lside(h)
|
| 218 |
+
|
| 219 |
+
return x+h # combine features of the node and features of its neighbours
|
| 220 |
+
|
| 221 |
+
# Single Layer Perceptron with batch norm., dropout and ReLU
|
| 222 |
+
class SLP(torch.nn.Module):
|
| 223 |
+
def __init__(self, inputs, outputs):
|
| 224 |
+
super().__init__()
|
| 225 |
+
|
| 226 |
+
self.l = Sequential(
|
| 227 |
+
Print(),
|
| 228 |
+
Dropout(DROPOUT_RATE),
|
| 229 |
+
Print(),
|
| 230 |
+
Linear(inputs, outputs),
|
| 231 |
+
BatchNorm(outputs),
|
| 232 |
+
ReLU()
|
| 233 |
+
)
|
| 234 |
+
self.norm = BatchNorm(outputs)
|
| 235 |
+
|
| 236 |
+
def forward(self, x):
|
| 237 |
+
# batch norm, dropout 0.2, relu
|
| 238 |
+
# x = x.dropout(DROPOUT_RATE)
|
| 239 |
+
x = self.l(x)
|
| 240 |
+
# x = self.norm(x).relu()
|
| 241 |
+
return x
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
class PlaNNet(torch.nn.Module):
|
| 248 |
+
"""possible names:
|
| 249 |
+
PCNN – Protein/Peptide/Polyamino-acid Convolutional NN. BUT: "Pulse Coupled NN"
|
| 250 |
+
CCNN - Conformation Convolutional NN. BUT: Constrained Convolutional NN
|
| 251 |
+
ACNN - polyAmino-acid Convolutional NN. BUT: Anatomically Constrained NN
|
| 252 |
+
PLN - Protein Learning (neural) Network
|
| 253 |
+
ACN (AACCNN) - Amino-Acid Chain-Convolutional NN
|
| 254 |
+
NNfP = NN for Proteins
|
| 255 |
+
PLearner = Protein Learner
|
| 256 |
+
PlaNNet /ˈplænet/ = Protein Learning Neural NETwork
|
| 257 |
+
"""
|
| 258 |
+
class EncodeAA:
|
| 259 |
+
def __call__(self, AAs):
|
| 260 |
+
return one_hot(AAs, AA_CLASSES).to(torch.float32)
|
| 261 |
+
class EmbedAA(torch.nn.Module):
|
| 262 |
+
_norm = None
|
| 263 |
+
|
| 264 |
+
def __init__(self, precomputed: bool = True):
|
| 265 |
+
super().__init__()
|
| 266 |
+
self._precomputed = precomputed
|
| 267 |
+
if precomputed:
|
| 268 |
+
vhse_coeffs = np.genfromtxt("code/VHSE.csv", delimiter=',', skip_header=1, usecols=range(1, VHSE_DIM+1))
|
| 269 |
+
vhse_coeffs = np.vstack([
|
| 270 |
+
vhse_coeffs,
|
| 271 |
+
np.zeros(vhse_coeffs.shape[1]) # 0s as the vector for 'X' AA
|
| 272 |
+
])
|
| 273 |
+
vhse_coeffs = torch.from_numpy(vhse_coeffs)
|
| 274 |
+
self.emb = Embedding.from_pretrained(vhse_coeffs)
|
| 275 |
+
else:
|
| 276 |
+
self.emb = Embedding(AA_CLASSES, VHSE_DIM) # embedding + batch_norm
|
| 277 |
+
self._norm = BatchNorm(VHSE_DIM)
|
| 278 |
+
|
| 279 |
+
def __call__(self, AAs):
|
| 280 |
+
emb = self.emb(AAs)
|
| 281 |
+
if self._norm:
|
| 282 |
+
self._norm(emb)
|
| 283 |
+
return emb
|
| 284 |
+
|
| 285 |
+
def __init__(self,
|
| 286 |
+
gl_pool: str = conf_dict['gl_pool'],
|
| 287 |
+
embed_aa: bool = bool(conf_dict['embed_aa']), # embedding (otherwise 1hot encoding)
|
| 288 |
+
embed_learn: bool = conf_dict['embed_aa'] == 'learn', # learn embedding (or precomputed VHSE)
|
| 289 |
+
L1_features: int = conf_dict['L1_features'],
|
| 290 |
+
cl_features: int = conf_dict['cl_features'],
|
| 291 |
+
**_):
|
| 292 |
+
super().__init__()
|
| 293 |
+
|
| 294 |
+
# MODEL HYPERPARAMETERS
|
| 295 |
+
# hidden layers features
|
| 296 |
+
L1C_FEATURES = L1_features
|
| 297 |
+
L2C_FEATURES = L1C_FEATURES*2
|
| 298 |
+
L3C_FEATURES = L2C_FEATURES*2
|
| 299 |
+
self.LF__FEATURES = L3C_FEATURES + (L3C_FEATURES if gl_pool == 'both' else 0) # avg (+ max)
|
| 300 |
+
|
| 301 |
+
self._gl_pool = gl_pool
|
| 302 |
+
torch.manual_seed(42)
|
| 303 |
+
|
| 304 |
+
self.AAenc = self.EmbedAA(not embed_learn) if embed_aa else self.EncodeAA()
|
| 305 |
+
|
| 306 |
+
# MODEL LAYERS
|
| 307 |
+
# don't do batch norm, ReLU - parameters
|
| 308 |
+
self.gcl3 = IEConv(VHSE_DIM if embed_aa else AA_CLASSES, L1C_FEATURES, 8)
|
| 309 |
+
# no pooling
|
| 310 |
+
self.gcl3_ = ResNet(L1C_FEATURES, L1C_FEATURES, 8)
|
| 311 |
+
# no pooling
|
| 312 |
+
self.gcl3__ = ResNet(L1C_FEATURES, L1C_FEATURES, 8)
|
| 313 |
+
# pooling
|
| 314 |
+
self.gcl4 = ResNet(L1C_FEATURES, L2C_FEATURES, 12)
|
| 315 |
+
# no pooling
|
| 316 |
+
self.gcl4_ = ResNet(L2C_FEATURES, L2C_FEATURES, 12)
|
| 317 |
+
# pooling
|
| 318 |
+
self.gcl5 = ResNet(L2C_FEATURES, L3C_FEATURES, 16)
|
| 319 |
+
# no pooling
|
| 320 |
+
self.gcl5_ = ResNet(L3C_FEATURES, L3C_FEATURES, 16)
|
| 321 |
+
# pooling
|
| 322 |
+
|
| 323 |
+
self.classifier = DLP(self.LF__FEATURES, cl_features, EC_CLASSES)
|
| 324 |
+
|
| 325 |
+
def forward(self,
|
| 326 |
+
AA_type,
|
| 327 |
+
coordinate,
|
| 328 |
+
seq_position,
|
| 329 |
+
axes,
|
| 330 |
+
batch_mask
|
| 331 |
+
):
|
| 332 |
+
batch_mask = batch_mask.to(torch.int64)
|
| 333 |
+
#print(AA_type)
|
| 334 |
+
AA_type = self.AAenc(AA_type).to(torch.float32)
|
| 335 |
+
# print(AA_type)
|
| 336 |
+
assert_test(AA_type)
|
| 337 |
+
# print(coordinate)
|
| 338 |
+
# print(batch_mask.shape, seq_position.shape, coordinate.shape)
|
| 339 |
+
seq_position = torch.reshape(seq_position.to(torch.float32), (-1,1))
|
| 340 |
+
# print(seq_position.view(-1))
|
| 341 |
+
assert_test(seq_position)
|
| 342 |
+
|
| 343 |
+
# 1st convolutional layer (AA level; 8-Å radius)
|
| 344 |
+
# print("ball query:", coordinate, coordinate.size, batch_mask)
|
| 345 |
+
neighbors = ball_query(coordinate, self.gcl3.distance, batch_mask) # [[tos] [froms]], e.g. [to0, to2, ...], [from1, from1, from2, ...]
|
| 346 |
+
# print(neighbors)
|
| 347 |
+
graphs = Data(
|
| 348 |
+
x = AA_type.to(torch.float32),
|
| 349 |
+
edge_index = neighbors,
|
| 350 |
+
pos = seq_position
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
assert_test(neighbors)
|
| 354 |
+
assert_test(coordinate)
|
| 355 |
+
# print(AA_type.shape )
|
| 356 |
+
h = self.gcl3(graphs.clone(), coordinate)
|
| 357 |
+
assert_test(h)
|
| 358 |
+
# print(h.shape)
|
| 359 |
+
graphs.x = h
|
| 360 |
+
h = self.gcl3_(graphs.clone(), coordinate)
|
| 361 |
+
# input() # DEBUG
|
| 362 |
+
self.act3 = h
|
| 363 |
+
graphs.x = h
|
| 364 |
+
h = self.gcl3__(graphs, coordinate)
|
| 365 |
+
self.act3 = h
|
| 366 |
+
#print(neighbors)
|
| 367 |
+
#h = self.gconv3(AA_type.to(torch.float32), neighbors)
|
| 368 |
+
# pooling
|
| 369 |
+
clusters = torch.div(seq_position.flatten(), 2, rounding_mode = "trunc")
|
| 370 |
+
#print(clusters)
|
| 371 |
+
clusters = batch_clusters(clusters, batch_mask)
|
| 372 |
+
#print(clusters)
|
| 373 |
+
#print(h.shape, coordinate.shape)
|
| 374 |
+
#print(batch_mask)
|
| 375 |
+
#print(coordinate)
|
| 376 |
+
coordinate, _ = avg_pool_x(clusters, coordinate, batch_mask)
|
| 377 |
+
h, _ = avg_pool_x(clusters, h, batch_mask)
|
| 378 |
+
clusters, batch_mask = avg_pool_x(clusters, clusters, batch_mask)
|
| 379 |
+
#print(coordinate)
|
| 380 |
+
#print(clusters, batch_mask)
|
| 381 |
+
#print(h.shape, coordinate.shape)
|
| 382 |
+
|
| 383 |
+
# 2nd convolutional layer (2 AAs level; 12-Å radius)
|
| 384 |
+
neighbors = ball_query(coordinate, self.gcl4.distance, batch_mask)
|
| 385 |
+
graphs = Data(
|
| 386 |
+
x = h,
|
| 387 |
+
edge_index = neighbors,
|
| 388 |
+
pos = torch.reshape(clusters, (-1,1))
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
h = self.gcl4(graphs.clone(), coordinate)
|
| 392 |
+
graphs.x = h
|
| 393 |
+
h = self.gcl4_(graphs, coordinate)
|
| 394 |
+
self.act4 = h
|
| 395 |
+
# h = self.gconv4(h, neighbors)
|
| 396 |
+
clusters = torch.div(clusters, 2, rounding_mode = "trunc")
|
| 397 |
+
# print(clusters)
|
| 398 |
+
clusters = batch_clusters(clusters, batch_mask)
|
| 399 |
+
|
| 400 |
+
coordinate, _ = avg_pool_x(clusters, coordinate, batch_mask)
|
| 401 |
+
h, _ = avg_pool_x(clusters, h, batch_mask)
|
| 402 |
+
clusters, batch_mask = avg_pool_x(clusters, clusters, batch_mask)
|
| 403 |
+
# print(clusters, batch_mask, clusters.shape)
|
| 404 |
+
# print(h.shape)
|
| 405 |
+
|
| 406 |
+
# 3rd convolutional layer (4 AAs level; 16-Å radius)
|
| 407 |
+
neighbors = ball_query(coordinate, self.gcl5.distance, batch_mask)
|
| 408 |
+
graphs = Data(
|
| 409 |
+
x = h,
|
| 410 |
+
edge_index = neighbors,
|
| 411 |
+
pos = torch.reshape(clusters, (-1,1))
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
h = self.gcl5(graphs.clone(), coordinate)
|
| 415 |
+
graphs.x = h
|
| 416 |
+
h = self.gcl5_(graphs, coordinate)
|
| 417 |
+
self.act5 = h
|
| 418 |
+
# h = self.gconv5(h, neighbors)
|
| 419 |
+
assert_test(h)
|
| 420 |
+
|
| 421 |
+
# global pooling
|
| 422 |
+
g = global_mean_pool(h, batch_mask)
|
| 423 |
+
if self._gl_pool == "both":
|
| 424 |
+
g2 = global_max_pool(h, batch_mask)
|
| 425 |
+
g = torch.stack([g, g2], 1)
|
| 426 |
+
g = torch.reshape(
|
| 427 |
+
g,
|
| 428 |
+
(-1, self.LF__FEATURES)
|
| 429 |
+
)
|
| 430 |
+
#print(h, h.shape)
|
| 431 |
+
#activations = self.act(h)
|
| 432 |
+
#print(activations)
|
| 433 |
+
assert_test(g)
|
| 434 |
+
# print('cl:', self.classifier(g))
|
| 435 |
+
|
| 436 |
+
return self.classifier(g)
|
| 437 |
+
|
| 438 |
+
class MutPred(torch.nn.Module):
|
| 439 |
+
def __init__(self,
|
| 440 |
+
base_nn: torch.nn.Module
|
| 441 |
+
):
|
| 442 |
+
super().__init__()
|
| 443 |
+
self.base_nn = base_nn
|
| 444 |
+
|
| 445 |
+
def forward(self,
|
| 446 |
+
AA_type,
|
| 447 |
+
coordinate,
|
| 448 |
+
seq_position,
|
| 449 |
+
axes,
|
| 450 |
+
batch_mask
|
| 451 |
+
):
|
| 452 |
+
base_pred = self.base_nn(AA_type, coordinate, seq_position, axes, batch_mask)
|
| 453 |
+
LOG(base_pred.view(-1), sep='\n')
|
| 454 |
+
pred = base_pred[1::2] - base_pred[0::2] # MUT - WT predictions
|
| 455 |
+
LOG(pred.sigmoid().view(-1))
|
| 456 |
+
return pred.sigmoid()
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
# log after a keyboard event (CTRL+BREAK on Windows)
|
| 460 |
+
class LOG:
|
| 461 |
+
def __init__(self):
|
| 462 |
+
LOG.on = False
|
| 463 |
+
return # TODO: signal.SIGQUIT does not exist on Windows Python 3.8
|
| 464 |
+
signal.signal(signal.SIGQUIT, self.signal_handler) # CTRL+\ on Linux (normally kills the process)
|
| 465 |
+
|
| 466 |
+
def __call__(self, *args, sep=' '):
|
| 467 |
+
if LOG.on is not False:
|
| 468 |
+
LOG.on = None
|
| 469 |
+
print(*args, sep=sep)
|
| 470 |
+
|
| 471 |
+
def signal_handler(*args):
|
| 472 |
+
print()
|
| 473 |
+
LOG.on = True
|
| 474 |
+
|
| 475 |
+
@staticmethod
|
| 476 |
+
def iter():
|
| 477 |
+
LOG.on = not not LOG.on
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def assert_test(tensor, mask = None):
|
| 481 |
+
if not mask:
|
| 482 |
+
# l = int(len(tensor) / conf_dict['norm_size']/2)
|
| 483 |
+
l = int(len(tensor) / 2)
|
| 484 |
+
# print(l)
|
| 485 |
+
# equal won't go well with small inaccuracies after ~7 significant digits
|
| 486 |
+
# assert torch.allclose(tensor[0:l], tensor[l:]), (tensor[0:l], tensor[l:])
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def Ensemble(paths_or_n): # Ensemble consisting of this-version models
|
| 490 |
+
return _Ensemble(paths_or_n, PlaNNet, MutPred)
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# online logging
|
| 494 |
+
LOG = LOG()
|
| 495 |
+
# LOG.on = True
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
|
code/predictor.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from code.data_preprocessing import process_pdb, mutate_seq
|
| 4 |
+
from code.data_loader import create_datapoint, collate_batch
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class EnsemblePredictor:
|
| 8 |
+
|
| 9 |
+
def __init__(self,
|
| 10 |
+
weights: str = "model_weights.pth", # trained model
|
| 11 |
+
version: str = None, # model version: '21_1'|...
|
| 12 |
+
):
|
| 13 |
+
conf_dict = torch.load(weights)
|
| 14 |
+
|
| 15 |
+
if version is None: # default model
|
| 16 |
+
from code.model_py import Ensemble
|
| 17 |
+
elif version == '21_2':
|
| 18 |
+
from code.model_py.v21_2 import Ensemble
|
| 19 |
+
elif version == '21_1':
|
| 20 |
+
from code.model_py.v21_1 import Ensemble
|
| 21 |
+
else:
|
| 22 |
+
raise 'Non-existing version!'
|
| 23 |
+
|
| 24 |
+
pred_model = Ensemble(5)
|
| 25 |
+
pred_model.load_state_dict(conf_dict)
|
| 26 |
+
pred_model.eval()
|
| 27 |
+
|
| 28 |
+
self._model = pred_model
|
| 29 |
+
|
| 30 |
+
def predict_change(self,
|
| 31 |
+
PDB_path: str,
|
| 32 |
+
chain: str,
|
| 33 |
+
aa_from: list,
|
| 34 |
+
locs: list,
|
| 35 |
+
aa_to: list
|
| 36 |
+
):
|
| 37 |
+
assert len(aa_from) == len(locs) == len(aa_to)
|
| 38 |
+
|
| 39 |
+
print("Processing structure...")
|
| 40 |
+
orig_seq, coords = process_pdb(PDB_path, chain)
|
| 41 |
+
mut_seq = mutate_seq(orig_seq, aa_from, locs, aa_to)
|
| 42 |
+
|
| 43 |
+
print("Calculating prediction...")
|
| 44 |
+
protein = create_datapoint(
|
| 45 |
+
'wt',
|
| 46 |
+
orig_seq,
|
| 47 |
+
coords
|
| 48 |
+
)
|
| 49 |
+
mutant = create_datapoint(
|
| 50 |
+
'mut',
|
| 51 |
+
mut_seq,
|
| 52 |
+
coords
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
data = collate_batch([protein, mutant])[:-2]
|
| 56 |
+
prediction = float(self._model.feed(data)[0])
|
| 57 |
+
prediction_cl = 'N' if round(prediction, 2) == 0.5 else\
|
| 58 |
+
'+' if prediction > 0.5 else '-'
|
| 59 |
+
|
| 60 |
+
return prediction_cl, prediction
|
model_weights.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7247bd1e89766a138815d96120f72bc03d77f0be1935886db44a8464683175dc
|
| 3 |
+
size 53058100
|
requirements.in
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--find-links "https://data.pyg.org/whl/torch-2.1.0+cpu.html"
|
| 2 |
+
biopython == 1.79 # < 1.82 # 1.82 removed three_to_one function https://github.com/biopython/biopython/pull/4508, 1.80 added the deprecation warning, TODO: internalize three_to_one function
|
| 3 |
+
tensorboard
|
| 4 |
+
torch ~= 2.1.0
|
| 5 |
+
torch-geometric
|
| 6 |
+
torch_scatter
|
| 7 |
+
torch_cluster
|
| 8 |
+
packaging # required by tensorboard but not properly specified in its requirements (probably due to being preinstalled from some version of pip)
|
requirements.txt
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Working configuration of Python 3.8 on Win10 with PyTorch 2.1 on CPU
|
| 2 |
+
--find-links "https://data.pyg.org/whl/torch-2.1.0+cpu.html"
|
| 3 |
+
absl-py==2.0.0
|
| 4 |
+
biopython==1.81
|
| 5 |
+
cachetools==5.3.2
|
| 6 |
+
certifi==2023.11.17
|
| 7 |
+
charset-normalizer==3.3.2
|
| 8 |
+
colorama==0.4.6
|
| 9 |
+
filelock==3.13.1
|
| 10 |
+
fsspec==2023.12.2
|
| 11 |
+
google-auth==2.26.2
|
| 12 |
+
google-auth-oauthlib==1.0.0
|
| 13 |
+
grpcio==1.60.0
|
| 14 |
+
idna==3.6
|
| 15 |
+
importlib-metadata==7.0.1
|
| 16 |
+
Jinja2==3.1.3
|
| 17 |
+
joblib==1.3.2
|
| 18 |
+
Markdown==3.5.2
|
| 19 |
+
MarkupSafe==2.1.3
|
| 20 |
+
mpmath==1.3.0
|
| 21 |
+
networkx==3.1
|
| 22 |
+
numpy==1.24.4
|
| 23 |
+
oauthlib==3.2.2
|
| 24 |
+
packaging==23.2
|
| 25 |
+
protobuf==4.25.2
|
| 26 |
+
psutil==5.9.7
|
| 27 |
+
pyasn1==0.5.1
|
| 28 |
+
pyasn1-modules==0.3.0
|
| 29 |
+
pyparsing==3.1.1
|
| 30 |
+
requests==2.31.0
|
| 31 |
+
requests-oauthlib==1.3.1
|
| 32 |
+
rsa==4.9
|
| 33 |
+
scikit-learn==1.3.2
|
| 34 |
+
scipy==1.10.1
|
| 35 |
+
sympy==1.12
|
| 36 |
+
tensorboard==2.14.0
|
| 37 |
+
tensorboard-data-server==0.7.2
|
| 38 |
+
threadpoolctl==3.2.0
|
| 39 |
+
torch==2.1.2
|
| 40 |
+
torch-cluster==1.6.3+pt21cpu
|
| 41 |
+
torch-scatter==2.1.2+pt21cpu
|
| 42 |
+
torch_geometric==2.4.0
|
| 43 |
+
tqdm==4.66.1
|
| 44 |
+
typing_extensions==4.9.0
|
| 45 |
+
urllib3==2.1.0
|
| 46 |
+
Werkzeug==3.0.1
|
| 47 |
+
zipp==3.17.0
|
wrapper.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
#---------------------------------------------------------------------
|
| 3 |
+
#--- Predictor of a protein solubility change given a mutation ---
|
| 4 |
+
#--- by Jan Velecky velda@mail.muni.cz ---
|
| 5 |
+
#--- Loschmidt Laboratories, 2023-24 ---
|
| 6 |
+
#--- example use: python3 wrapper.py -h ---
|
| 7 |
+
#---------------------------------------------------------------------
|
| 8 |
+
import argparse
|
| 9 |
+
from functools import partial
|
| 10 |
+
|
| 11 |
+
import Bio.PDB.Polypeptide as AA
|
| 12 |
+
from code.data_preprocessing import get_PDB
|
| 13 |
+
|
| 14 |
+
# ---------------------------------------- DATA TYPES FOR ARGPARSE ----------------------------------------
|
| 15 |
+
def Check_nonNegative(value):
|
| 16 |
+
ivalue = int(value)
|
| 17 |
+
if ivalue < 0:
|
| 18 |
+
return False
|
| 19 |
+
return ivalue
|
| 20 |
+
|
| 21 |
+
# non-negative integer datatype
|
| 22 |
+
def Type_nonNegative(value):
|
| 23 |
+
ivalue = int(value)
|
| 24 |
+
if ivalue < 0:
|
| 25 |
+
raise argparse.ArgumentTypeError("%s is not a non-negative integer" % value)
|
| 26 |
+
return ivalue
|
| 27 |
+
|
| 28 |
+
def Type_char(value):
|
| 29 |
+
if len(value) != 1:
|
| 30 |
+
raise argparse.ArgumentTypeError("'%s' is not a character" % value)
|
| 31 |
+
return value
|
| 32 |
+
|
| 33 |
+
def Type_aminoAcid(value):
|
| 34 |
+
orig = value
|
| 35 |
+
value = str.upper(value)
|
| 36 |
+
if len(value) == 1:
|
| 37 |
+
try:
|
| 38 |
+
value = AA.one_to_three(value)
|
| 39 |
+
except:
|
| 40 |
+
pass
|
| 41 |
+
if not AA.is_aa(value):
|
| 42 |
+
raise argparse.ArgumentTypeError("'%s' is not a valid amino-acid" % orig)
|
| 43 |
+
return str.upper(AA.three_to_one(value))
|
| 44 |
+
|
| 45 |
+
def parseList(s, type):
|
| 46 |
+
return [type(i) for i in s.split(',')]
|
| 47 |
+
|
| 48 |
+
def Type_listOf(basetype):
|
| 49 |
+
return partial(parseList, type=basetype)
|
| 50 |
+
|
| 51 |
+
def Type_PDB(pdb_code):
|
| 52 |
+
try:
|
| 53 |
+
pdb_path = get_PDB(pdb_code.lower())
|
| 54 |
+
except Exception as e:
|
| 55 |
+
raise argparse.ArgumentTypeError(e)
|
| 56 |
+
return pdb_code, pdb_path
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# ----------------------------------------------END OF DATA TYPES ------------------------------------------
|
| 60 |
+
|
| 61 |
+
argParser = argparse.ArgumentParser(add_help = True,
|
| 62 |
+
description=
|
| 63 |
+
"""Solubility change preditor:
|
| 64 |
+
Predicts the change in the solubility of a given protein variant
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
""", epilog="""
|
| 68 |
+
Amino acids can be specified by both 1- or 3-letter code.
|
| 69 |
+
|
| 70 |
+
example of use: ./wrapper.py 1EER F,R 48,150 D --verbose
|
| 71 |
+
""",conflict_handler='resolve', # overwrite conflicts
|
| 72 |
+
formatter_class=argparse.RawTextHelpFormatter,
|
| 73 |
+
prefix_chars="+-",
|
| 74 |
+
# exit_on_error=False # would be nice to use it for prompting a user for required params, but was implemented buggy: https://bugs.python.org/issue41255
|
| 75 |
+
)
|
| 76 |
+
argParser.add_argument(
|
| 77 |
+
'input',
|
| 78 |
+
metavar='pdb-code',
|
| 79 |
+
type=Type_PDB,
|
| 80 |
+
help="PDB code"
|
| 81 |
+
)
|
| 82 |
+
argParser.add_argument( # we only process one chain in the end
|
| 83 |
+
'chain', nargs="?",
|
| 84 |
+
default='A',
|
| 85 |
+
type=Type_char,
|
| 86 |
+
help="target chain character; default=A"
|
| 87 |
+
)
|
| 88 |
+
argParser.add_argument(
|
| 89 |
+
'orig',
|
| 90 |
+
metavar='wild-type',
|
| 91 |
+
type=Type_listOf(Type_aminoAcid),
|
| 92 |
+
help="wild-type residue(s) amino-acid[n]"
|
| 93 |
+
)
|
| 94 |
+
argParser.add_argument(
|
| 95 |
+
'loc',
|
| 96 |
+
metavar='location',
|
| 97 |
+
type=Type_listOf(int),
|
| 98 |
+
help="mutated position(s) integer[n]"
|
| 99 |
+
)
|
| 100 |
+
argParser.add_argument(
|
| 101 |
+
'mut',
|
| 102 |
+
metavar='mutation',
|
| 103 |
+
type=Type_listOf(Type_aminoAcid),
|
| 104 |
+
help="1 or n substituents amino-acid|amino-acid[n]"
|
| 105 |
+
)
|
| 106 |
+
argParser.add_argument(
|
| 107 |
+
'-v', '--verbose',
|
| 108 |
+
action="store_true",
|
| 109 |
+
#help="show ouputs from the underlaying tools"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
argParser.add_argument('--ver', default=None, help=argparse.SUPPRESS)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == '__main__':
|
| 120 |
+
# ---------------------------------------- ARGUMENTS PROCESSING ----------------------------------------
|
| 121 |
+
args = argParser.parse_args()
|
| 122 |
+
|
| 123 |
+
# positional arguments parsing (argparse can't cope with nested positional arguments)
|
| 124 |
+
if len(args.loc) != len(args.mut):
|
| 125 |
+
if len(args.mut) == 1: # same target AA on all specified positions
|
| 126 |
+
args.mut = args.mut * len(args.loc)
|
| 127 |
+
else:
|
| 128 |
+
argParser.error("Inconsistent multi-point mutant specification")
|
| 129 |
+
pdb_code, pdb_path = args.input
|
| 130 |
+
chain = args.chain
|
| 131 |
+
|
| 132 |
+
# import code.data_preprocessing.model as modeling
|
| 133 |
+
# if(args.verbose):
|
| 134 |
+
# modeling.VERBOSE_LEVEL = modeling.VERBOSE_VERBOSE
|
| 135 |
+
|
| 136 |
+
# ------------------------------------- PREPROCESSING & INFERENCE --------------------------------------
|
| 137 |
+
from code.predictor import EnsemblePredictor # expensive import left for after the argument check
|
| 138 |
+
|
| 139 |
+
pred_model = EnsemblePredictor(version=args.ver)
|
| 140 |
+
|
| 141 |
+
assesment, prediction = pred_model.predict_change(pdb_path, chain, args.orig, args.loc, args.mut)
|
| 142 |
+
assesment = {'+': 'solubilizing', 'N': 'neutral', '-': 'desolubilizing'}[assesment]
|
| 143 |
+
print()
|
| 144 |
+
print("Predicted solubility change: %g (%s)" % (prediction, assesment))
|