vvelda commited on
Commit
b140e2c
·
verified ·
1 Parent(s): 9381190

Initial commit

Browse files
README.md CHANGED
@@ -1,14 +1,72 @@
1
- ---
2
- title: SoluProtMutDemo
3
- emoji: 📈
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: 5.46.0
8
- app_file: app.py
9
- pinned: false
10
- license: bsd-3-clause
11
- short_description: Predictor of a protein solubility change given a mutation
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))