File size: 10,366 Bytes
6963cf4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e29637
 
8bd6bbb
 
11bcc1a
a28eeb5
11bcc1a
e0408f3
6963cf4
8bd6bbb
 
 
a6b7cf0
 
 
4ed9ef0
a6b7cf0
 
aae512c
 
 
6963cf4
a28eeb5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6b7cf0
6963cf4
 
 
a6b7cf0
6963cf4
 
 
 
 
 
 
 
a6b7cf0
6963cf4
 
 
 
 
 
 
 
 
 
a6b7cf0
 
 
 
6963cf4
a6b7cf0
 
 
 
 
 
a2460df
6963cf4
 
 
 
 
a6b7cf0
 
6963cf4
a6b7cf0
 
 
 
 
 
 
 
 
 
 
 
6963cf4
 
a6b7cf0
6963cf4
 
 
a6b7cf0
 
 
 
6643342
a6b7cf0
 
 
6643342
a6b7cf0
6963cf4
6643342
6963cf4
11bcc1a
6963cf4
01ff8b6
a6b7cf0
01ff8b6
 
 
a6b7cf0
 
01ff8b6
a6b7cf0
01ff8b6
 
a6b7cf0
01ff8b6
 
 
 
 
 
a6b7cf0
 
01ff8b6
 
a6b7cf0
c5491c0
8bd6bbb
c5491c0
 
8bd6bbb
 
 
 
11bcc1a
01ff8b6
e0408f3
a28eeb5
8bd6bbb
a28eeb5
 
5dae6ce
11bcc1a
a28eeb5
8bd6bbb
11bcc1a
a28eeb5
a653779
11bcc1a
 
8bd6bbb
11bcc1a
 
8bd6bbb
e0408f3
 
 
8bd6bbb
c5491c0
 
e0408f3
c5491c0
e0408f3
 
 
8bd6bbb
e0408f3
 
8bd6bbb
e0408f3
c5491c0
 
 
 
 
 
 
 
 
 
 
e0408f3
 
 
 
 
8bd6bbb
 
e4000f9
a6b7cf0
01ff8b6
 
8bd6bbb
01ff8b6
 
 
8bd6bbb
11bcc1a
 
01ff8b6
 
8bd6bbb
01ff8b6
 
 
 
8bd6bbb
 
01ff8b6
 
8bd6bbb
 
5dae6ce
01ff8b6
 
 
 
 
11bcc1a
088d0d1
 
01ff8b6
11bcc1a
5dae6ce
01ff8b6
 
a28eeb5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import gradio as gr
from model_loader import load_model

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader

import re
import numpy as np
import os
import pandas as pd
import copy

import transformers, datasets
from transformers import AutoTokenizer
from transformers import DataCollatorForTokenClassification

from datasets import Dataset

from scipy.special import expit

import requests

from gradio_molecule3d import Molecule3D

# Biopython imports
from Bio.PDB import PDBParser, Select, PDBIO
from Bio.PDB.DSSP import DSSP
from Bio.PDB import PDBList

from matplotlib import cm  # For color mapping
from matplotlib.colors import Normalize

# Configuration
checkpoint = 'ThorbenF/prot_t5_xl_uniref50'
max_length = 1500

# Load model and move to device
model, tokenizer = load_model(checkpoint, max_length)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model.to(device)
model.eval()

def is_valid_sequence_length(length: int) -> bool:
    """Check if sequence length is within valid range."""
    return 100 <= length <= 1500

def is_nucleic_acid_chain(chain) -> bool:
    """Check if chain contains nucleic acids."""
    nucleic_acids = {'A', 'C', 'G', 'T', 'U', 'DA', 'DC', 'DG', 'DT', 'DU', 'UNK'}
    return any(residue.get_resname().strip() in nucleic_acids for residue in chain)

def extract_protein_sequence(pdb_path):
    """
    Extract the longest protein sequence from a PDB file with improved logic
    """
    parser = PDBParser(QUIET=1)
    structure = parser.get_structure('protein', pdb_path)
    
    # Comprehensive amino acid mapping
    aa_dict = {
        # Standard amino acids (20 canonical)
        'ALA': 'A', 'CYS': 'C', 'ASP': 'D', 'GLU': 'E', 'PHE': 'F',
        'GLY': 'G', 'HIS': 'H', 'ILE': 'I', 'LYS': 'K', 'LEU': 'L',
        'MET': 'M', 'ASN': 'N', 'PRO': 'P', 'GLN': 'Q', 'ARG': 'R',
        'SER': 'S', 'THR': 'T', 'VAL': 'V', 'TRP': 'W', 'TYR': 'Y',
        
        # Modified amino acids and alternative names
        'MSE': 'M',  # Selenomethionine
        'SEP': 'S',  # Phosphoserine
        'TPO': 'T',  # Phosphothreonine
        'CSO': 'C',  # Hydroxylalanine
        'PTR': 'Y',  # Phosphotyrosine
        'HYP': 'P',  # Hydroxyproline
    }
    
    # Ligand and nucleic acid exclusion set
    ligand_exclusion_set = {'HOH', 'WAT', 'DOD', 'SO4', 'PO4', 'GOL', 'ACT', 'EDO'}
    
    # Find the longest protein chain
    longest_sequence = ""
    longest_chain = None
    
    for model in structure:
        for chain in model:
            # Skip nucleic acid chains
            if is_nucleic_acid_chain(chain):
                continue
            
            # Extract and convert sequence
            sequence = ""
            for residue in chain:
                # Check if residue is a standard amino acid or a known modified amino acid
                res_name = residue.get_resname().strip()
                if res_name in aa_dict:
                    sequence += aa_dict[res_name]
            
            # Check for valid length and update longest sequence
            if (10 < len(sequence) < 1500 and 
                len(sequence) > len(longest_sequence)):
                longest_sequence = sequence
                longest_chain = chain
    
    if not longest_sequence:
        return None, None, pdb_path
    
    # Save filtered PDB if needed
    if longest_chain:
        io = PDBIO()
        io.set_structure(longest_chain.get_parent().get_parent())
        filtered_pdb_path = pdb_path.replace('.pdb', '_filtered.pdb')
        io.save(filtered_pdb_path)
        return longest_sequence, longest_chain, filtered_pdb_path
    
    return longest_sequence, longest_chain, pdb_path

def create_dataset(tokenizer, seqs, labels, checkpoint):
    tokenized = tokenizer(seqs, max_length=max_length, padding=False, truncation=True)
    dataset = Dataset.from_dict(tokenized)
    
    # Adjust labels based on checkpoint
    if ("esm" in checkpoint) or ("ProstT5" in checkpoint):
        labels = [l[:max_length-2] for l in labels] 
    else:
        labels = [l[:max_length-1] for l in labels] 
        
    dataset = dataset.add_column("labels", labels)
     
    return dataset

def convert_predictions(input_logits):
    all_probs = []
    for logits in input_logits:
        logits = logits.reshape(-1, 2)
        probabilities_class1 = expit(logits[:, 1] - logits[:, 0])
        all_probs.append(probabilities_class1)
        
    return np.concatenate(all_probs)

def normalize_scores(scores):
    min_score = np.min(scores)
    max_score = np.max(scores)
    return (scores - min_score) / (max_score - min_score) if max_score > min_score else scores

def predict_protein_sequence(test_one_letter_sequence):    
    # Sanitize input sequence
    test_one_letter_sequence = test_one_letter_sequence.replace("O", "X") \
        .replace("B", "X").replace("U", "X") \
        .replace("Z", "X").replace("J", "X")
    
    # Prepare sequence for different model types
    if ("prot_t5" in checkpoint) or ("ProstT5" in checkpoint):
        test_one_letter_sequence = " ".join(test_one_letter_sequence)
    
    if "ProstT5" in checkpoint:
        test_one_letter_sequence = "<AA2fold> " + test_one_letter_sequence
    
    # Create dummy labels 
    dummy_labels = [np.zeros(len(test_one_letter_sequence))]
    
    # Create dataset
    test_dataset = create_dataset(tokenizer, 
                                  [test_one_letter_sequence], 
                                  dummy_labels, 
                                  checkpoint)
    
    # Select appropriate data collator
    data_collator = (DataCollatorForTokenClassification(tokenizer) 
                     if "esm" not in checkpoint and "ProstT5" not in checkpoint 
                     else DataCollatorForTokenClassification(tokenizer))
    
    # Create data loader
    test_loader = DataLoader(test_dataset, batch_size=1, collate_fn=data_collator)
    
    # Predict
    for batch in test_loader:
        input_ids = batch['input_ids'].to(device)
        attention_mask = batch['attention_mask'].to(device)
        
        with torch.no_grad():
            outputs = model(input_ids, attention_mask=attention_mask)
            logits = outputs.logits.detach().cpu().numpy()

    # Process logits
    logits = logits[:, :-1]  # Remove last element for prot_t5
    logits = convert_predictions(logits)
      
    # Normalize and format results
    normalized_scores = normalize_scores(logits)
    test_one_letter_sequence = test_one_letter_sequence.replace(" ", "")
    
    return test_one_letter_sequence, normalized_scores

def fetch_pdb(pdb_id):
    try:
        # Create a directory to store PDB files if it doesn't exist
        os.makedirs('pdb_files', exist_ok=True)
        
        # Fetch the PDB structure from RCSB
        pdb_url = f'https://files.rcsb.org/download/{pdb_id}.pdb'
        pdb_path = f'pdb_files/{pdb_id}.pdb'
        
        # Download the file
        response = requests.get(pdb_url)
        
        if response.status_code == 200:
            with open(pdb_path, 'wb') as f:
                f.write(response.content)
            return pdb_path
        else:
            return None
    
    except Exception as e:
        print(f"Error fetching PDB: {e}")
        return None


def score_to_color(score):
    norm = Normalize(vmin=0, vmax=1)  # Normalize scores between 0 and 1
    color_map = cm.coolwarm  # Directly use the colormap (e.g., 'cividis', 'coolwarm', etc.)
    rgba = color_map(norm(score))  # Get RGBA values
    hex_color = '#{:02x}{:02x}{:02x}'.format(int(rgba[0] * 255), int(rgba[1] * 255), int(rgba[2] * 255))
    return hex_color

def process_pdb(pdb_id):
    # Fetch PDB file
    pdbl = PDBList()
    pdb_path = pdbl.retrieve_pdb_file(pdb_id, pdir='pdb_files', file_format='pdb')

    if not pdb_path or not os.path.exists(pdb_path):
        return "Failed to fetch PDB file", None

    # Extract protein sequence and chain
    protein_sequence, chain, filtered_pdb_path = extract_protein_sequence(pdb_path)

    if not protein_sequence:
        return "No suitable protein sequence found", None

    # Predict binding sites
    sequence, normalized_scores = predict_protein_sequence(protein_sequence)

    # Prepare result string
    result_str = "\n".join([f"{aa}: {score:.2f}" for aa, score in zip(sequence, normalized_scores)])

    # Prepare representations for Molecule3D
    reps = [
        {
            "model": 0,
            "chain": "",
            "resname": "",
            "style": "cartoon",
            "color": "spectrum",
            "residue_range": "",
            "around": 0,
            "byres": False,
            "visible": True
        }
    ]

    # Add color-coded residues based on binding site scores
    #for i, score in enumerate(normalized_scores):
    #    if score > 0.7:  # You can adjust this threshold
    #        reps.append({
    #            "model": 0,
    #            "chain": chain.get_id(),
    #            "style": "stick",
    #            "color": score_to_color(score),
    #            "residue_range": f"{i+1}-{i+1}",
    #            "byres": True,
    #            "visible": True
    #        })

    # Create Molecule3D with the PDB file and representations
    molecule_viewer = Molecule3D(
        reps=reps
    )

    return result_str, molecule_viewer

# Create Gradio interface
with gr.Blocks() as demo:
    gr.Markdown("# Protein Binding Site Prediction")

    with gr.Row():
        with gr.Column():
            pdb_input = gr.Textbox(
                value="2IWI",
                label="PDB ID",
                placeholder="Enter PDB ID here..."
            )
            predict_btn = gr.Button("Predict Binding Sites")

        with gr.Column():
            predictions_output = gr.Textbox(
                label="Binding Site Predictions"
            )
            molecule_output = Molecule3D(label="Protein Structure")

    # Prediction logic
    predict_btn.click(
        process_pdb,
        inputs=[pdb_input],
        outputs=[predictions_output, molecule_output]
    )

    gr.Markdown("## Examples")
    gr.Examples(
        examples=[
            ["2IWI"],
            ["7RPZ"],
            ["3TJN"]
        ],
        inputs=[pdb_input],
        outputs=[predictions_output, molecule_output]
    )

demo.launch()