Fast-HInt-ppi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Fast_HInt/File_proteins.py +780 -0
- Fast_HInt/HInt.py +244 -0
- Fast_HInt/Scoring_HInt.py +954 -0
- Fast_HInt/Utils_HInt.py +1313 -0
- Fast_HInt/__init__.py +1 -0
- Fast_HInt/get_good_inter_pae.py +340 -0
- fast_hint_ppi-0.1.0.dist-info/METADATA +33 -0
- fast_hint_ppi-0.1.0.dist-info/RECORD +11 -0
- fast_hint_ppi-0.1.0.dist-info/WHEEL +5 -0
- fast_hint_ppi-0.1.0.dist-info/entry_points.txt +2 -0
- fast_hint_ppi-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
""" Scoring file of HInt
|
|
2
|
+
|
|
3
|
+
Author: Quentin Rouger
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import csv
|
|
7
|
+
import logging
|
|
8
|
+
import multiprocessing
|
|
9
|
+
import HInt.get_good_inter_pae
|
|
10
|
+
import gc
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import json
|
|
13
|
+
import pickle
|
|
14
|
+
import gzip
|
|
15
|
+
import numpy as np
|
|
16
|
+
import matplotlib
|
|
17
|
+
matplotlib.use('Agg')
|
|
18
|
+
from matplotlib import pyplot as plt
|
|
19
|
+
import copy
|
|
20
|
+
import string
|
|
21
|
+
import subprocess
|
|
22
|
+
import math
|
|
23
|
+
import shutil
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from multiprocessing import Pool
|
|
26
|
+
from tqdm import tqdm
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from scipy.special import softmax
|
|
29
|
+
from Bio import PDB
|
|
30
|
+
|
|
31
|
+
# Configure global logger
|
|
32
|
+
logging.basicConfig(
|
|
33
|
+
filename="./log_file/HInt.log", # Log file name
|
|
34
|
+
level=logging.INFO, # Log level
|
|
35
|
+
format="%(asctime)s - %(levelname)s - %(message)s" # Log format
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger()
|
|
39
|
+
|
|
40
|
+
def Score_interaction (file, Informations_dict, CPU, Interaction, job="", multi_scoring="False", bait="") :
|
|
41
|
+
"""
|
|
42
|
+
Compute interaction scores (PPI or homo-oligomer) from AlphaFold predictions, aggregate them into meaningful metrics (iQ_score / hiQ_score), and update the list of valid prey proteins accordingly.
|
|
43
|
+
|
|
44
|
+
- detects which interactions still need to be scored
|
|
45
|
+
- runs scoring in parallel (CPU multiprocessing)
|
|
46
|
+
- merges all results into CSV files
|
|
47
|
+
- filters out bad interactions based on inter-PAE / interface quality
|
|
48
|
+
|
|
49
|
+
Parameters :
|
|
50
|
+
----------
|
|
51
|
+
file : object of class File_proteins
|
|
52
|
+
Informations_dict : dictionary
|
|
53
|
+
CPU : int
|
|
54
|
+
Interaction : str
|
|
55
|
+
Interaction type ("PPI_int" or "homo_int")
|
|
56
|
+
bait : str
|
|
57
|
+
multi_scoring : str
|
|
58
|
+
|
|
59
|
+
Return :
|
|
60
|
+
----------
|
|
61
|
+
len(ppi_list)
|
|
62
|
+
"""
|
|
63
|
+
result_dict = file.get_result_dict()
|
|
64
|
+
int_score = file.get_int_score() #saved scores
|
|
65
|
+
homo_score = file.get_homo_score()
|
|
66
|
+
new_possible_prey = list()
|
|
67
|
+
Path_ccp4 = Informations_dict["Path_ccp4"]
|
|
68
|
+
regions = Informations_dict["Regions"]
|
|
69
|
+
nbr_homo = Informations_dict["Homo-oligomer"]
|
|
70
|
+
AF_version = Informations_dict["AlphaFold"]
|
|
71
|
+
Compounds = file.get_compounds().keys()
|
|
72
|
+
ppi_list = list()
|
|
73
|
+
already_done = list()
|
|
74
|
+
already_dict = dict()
|
|
75
|
+
mean_iQ_score = dict()
|
|
76
|
+
cv_iQ_score = dict()
|
|
77
|
+
|
|
78
|
+
for t in ["micromamba", "mamba", "conda"] :
|
|
79
|
+
if shutil.which(t) :
|
|
80
|
+
tool = t
|
|
81
|
+
|
|
82
|
+
result = subprocess.run([tool, "env", "list", "--json"],capture_output=True, text=True, check=True)
|
|
83
|
+
envs = json.loads(result.stdout)["envs"]
|
|
84
|
+
exists = any("pi_score" in env for env in envs)
|
|
85
|
+
if not exists :
|
|
86
|
+
subprocess.run([tool, "create", "-y", "-n", "pi_score","python=2.7", "scikit-learn=0.20.4", "biopython", "biopandas"], check=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if bait == "" and job != "" :
|
|
90
|
+
bait = job.replace(job.split(";")[-1],"").strip(";")
|
|
91
|
+
possible_prey = [job.split(";")[-1].strip("\n")]
|
|
92
|
+
if bait != "" : #setup bait name
|
|
93
|
+
bait_name = bait.replace(";","_and_")
|
|
94
|
+
for prot in bait.split(";") :
|
|
95
|
+
if regions[prot] != "0-0" :
|
|
96
|
+
start = int(regions[prot].split("-")[0])
|
|
97
|
+
end = int(regions[prot].split("-")[1])
|
|
98
|
+
bait_name = bait_name.replace(prot,f"{prot}_{start}-{end}")
|
|
99
|
+
possible_prey = file.get_possible_prey()
|
|
100
|
+
|
|
101
|
+
#Multiprocessing to score all interactions
|
|
102
|
+
if os.path.isdir(f"./result_{Interaction}") == True :
|
|
103
|
+
if Interaction == "homo_int" : #for homo-oligomer
|
|
104
|
+
for protein in possible_prey :
|
|
105
|
+
if f"hiQ_score_{nbr_homo}er" not in homo_score[protein].keys() :
|
|
106
|
+
ppi_list.append(f"./result_{Interaction}/{protein}_homo_{nbr_homo}er") #Found a solution for score only homo-oligomer without score
|
|
107
|
+
else :
|
|
108
|
+
result_dict[protein][f"hiQ_score_{nbr_homo}er"] = homo_score[protein][f"hiQ_score_{nbr_homo}er"]
|
|
109
|
+
if homo_score[protein][f"hiQ_score_{nbr_homo}er"] > 0 :
|
|
110
|
+
new_possible_prey.append(protein)
|
|
111
|
+
if homo_score[protein][f"hiQ_score_{nbr_homo}er"] == 0 :
|
|
112
|
+
result_dict[protein]["Reason_for_filtering"] = f"Bad homo-oligomer PAE : AF"
|
|
113
|
+
if Interaction == "PPI_int" : #for one vs all
|
|
114
|
+
for protein in possible_prey : #check if protein is already score
|
|
115
|
+
if f"iQ_score_vs_{bait_name}" not in int_score[protein].keys() :
|
|
116
|
+
dir_path = Path(f"./result_{Interaction}/{bait_name}_and_{protein}")
|
|
117
|
+
inv_dir = Path(f"./result_{Interaction}/{protein}_and_{bait_name}")
|
|
118
|
+
if dir_path.exists() and dir_path.is_dir() :
|
|
119
|
+
ppi_list.append(f"./result_{Interaction}/{bait_name}_and_{protein}")
|
|
120
|
+
elif inv_dir.exists() and inv_dir.is_dir() :
|
|
121
|
+
ppi_list.append(f"./result_{Interaction}/{protein}_and_{bait_name}")
|
|
122
|
+
else :
|
|
123
|
+
result_dict[protein][f"iQ_score_vs_{bait_name}"] = int_score[protein][f"iQ_score_vs_{bait_name}"]
|
|
124
|
+
if int_score[protein][f"iQ_score_vs_{bait_name}"] > 0 :
|
|
125
|
+
new_possible_prey.append(protein)
|
|
126
|
+
if int_score[protein][f"iQ_score_vs_{bait_name}"] == 0 :
|
|
127
|
+
result_dict[protein]["Reason_for_filtering"] = f"Bad interactions with {bait_name} : inter PAE > 10 A"
|
|
128
|
+
if Interaction == "Compounds" : #for compounds
|
|
129
|
+
for compound in Compounds :
|
|
130
|
+
if f"iQ_score_vs_{compound}" not in int_score[compound].keys() :
|
|
131
|
+
dir_path = Path(f"./result_{Interaction}/{bait_name}_and_{compound}")
|
|
132
|
+
inv_dir = Path(f"./result_{Interaction}/{compound}_and_{bait_name}")
|
|
133
|
+
if dir_path.exists() and dir_path.is_dir() :
|
|
134
|
+
ppi_list.append(f"./result_{Interaction}/{bait_name}_and_{compound}")
|
|
135
|
+
elif inv_dir.exists() and inv_dir.is_dir() :
|
|
136
|
+
ppi_list.append(f"./result_{Interaction}/{compound}_and_{bait_name}")
|
|
137
|
+
else :
|
|
138
|
+
result_dict[compound][f"iQ_score_vs_{compound}"] = int_score[compound][f"iQ_score_vs_{compound}"]
|
|
139
|
+
if int_score[compound][f"iQ_score_vs_{compound}"] == 0 :
|
|
140
|
+
result_dict[compound]["Reason_for_filtering"] = f"Bad interactions with {bait_name} : inter PAE > 10 A"
|
|
141
|
+
results = []
|
|
142
|
+
with multiprocessing.Pool(CPU) as pool : #just run scoring for interactions without score
|
|
143
|
+
tasks = [(ppi, file, AF_version, Path_ccp4, multi_scoring) for ppi in ppi_list]
|
|
144
|
+
results_iter = pool.imap_unordered(run_scoring, tasks)
|
|
145
|
+
for df in tqdm(results_iter, total=len(ppi_list), desc="Scoring interactions") :
|
|
146
|
+
if df is not None and not df.empty :
|
|
147
|
+
results.append(df)
|
|
148
|
+
pool.close()
|
|
149
|
+
pool.join()
|
|
150
|
+
|
|
151
|
+
merged_df = pd.concat(results, ignore_index=True) if results else pd.DataFrame() #write an empty dataframe if no result
|
|
152
|
+
merged_df.to_csv(os.path.join(f"./result_{Interaction}", "predictions_with_good_interpae.csv"), index=False)
|
|
153
|
+
if ppi_list : #Add result in dict result if there is new ppi scored
|
|
154
|
+
#Resume all score and set new possible prey
|
|
155
|
+
with open(f"result_{Interaction}/predictions_with_good_interpae.csv", "r") as result_file :
|
|
156
|
+
reader = csv.DictReader(result_file)
|
|
157
|
+
|
|
158
|
+
#For one vs all
|
|
159
|
+
if Interaction == "PPI_int" or Interaction == "Compounds" : #make int_score
|
|
160
|
+
all_lines = "jobs,pi_score,iptm_ptm,pDockQ,iQ_score\n"
|
|
161
|
+
for row in reader :
|
|
162
|
+
job = row["jobs"]
|
|
163
|
+
just_name = job.split("_ranked_")[0]
|
|
164
|
+
prey_name = just_name.replace(bait_name,"").replace("_and_","")
|
|
165
|
+
if '_and_' in job and prey_name in possible_prey and bait_name in job : #check if interaction is a PPI and if prey is in possible prey list
|
|
166
|
+
if "," in bait : #multimer bait
|
|
167
|
+
if row["pi_score"] == 'No interface detected' :
|
|
168
|
+
if job in already_done : #if protein have multi interface interaction, mean of pi_score #multimeric bait
|
|
169
|
+
all_lines = '\n'.join(all_lines.rstrip('\n').split('\n')[:-1]) #delete last line
|
|
170
|
+
already_dict[job].append(float(-2.63))
|
|
171
|
+
mean_pi_score = (sum(already_dict[job]) + float(-2.63)) / len(already_dict[job])
|
|
172
|
+
iQ_score = ((mean_pi_score+2.63)/5.26)*60+float(row["iptm_ptm"])*40
|
|
173
|
+
line =f'\n{just_name},{str(mean_pi_score)},{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
174
|
+
else :
|
|
175
|
+
mean_iQ_score[just_name.split("_and_")[-1]] = []
|
|
176
|
+
already_dict[job] = [float(-2.63)]
|
|
177
|
+
iQ_score = float(row["iptm_ptm"])*30#pi_score don't detect interface so it's set on -2.63
|
|
178
|
+
line =f'{just_name},-2.63,{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
179
|
+
already_done.append(job)
|
|
180
|
+
else :
|
|
181
|
+
if job in already_done : #if protein have multi interface interaction, mean of pi_score #multimeric bait
|
|
182
|
+
all_lines = '\n'.join(all_lines.rstrip('\n').split('\n')[:-1]) #delete last line
|
|
183
|
+
already_dict[job].append(float(row["pi_score"]))
|
|
184
|
+
mean_pi_score = (sum(already_dict[job]) + float(row["pi_score"])) / len(already_dict[job])
|
|
185
|
+
iQ_score = ((mean_pi_score+2.63)/5.26)*60+float(row["iptm_ptm"])*40
|
|
186
|
+
line =f'\n{just_name},{str(mean_pi_score)},{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
187
|
+
else :
|
|
188
|
+
mean_iQ_score[just_name.split("_and_")[-1]] = []
|
|
189
|
+
already_dict[job] = [float(row["pi_score"])]
|
|
190
|
+
iQ_score = ((float(row["pi_score"])+2.63)/5.26)*60+float(row["iptm_ptm"])*40
|
|
191
|
+
line =f'{just_name},{row["pi_score"]},{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
192
|
+
already_done.append(job)
|
|
193
|
+
|
|
194
|
+
else : #if only one bait
|
|
195
|
+
if row["pi_score"] == 'No interface detected' :
|
|
196
|
+
iQ_score = float(row["iptm_ptm"])*30+float(row["mpDockQ/pDockQ"])*30 #pi_score don't detect interface so it's set on -2.63
|
|
197
|
+
if "ranked_0" in job :
|
|
198
|
+
line =f'{just_name},-2.63,{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
199
|
+
mean_iQ_score[prey_name] = []
|
|
200
|
+
else :
|
|
201
|
+
iQ_score = ((float(row["pi_score"])+2.63)/5.26)*40+float(row["iptm_ptm"])*30+float(row["mpDockQ/pDockQ"])*30
|
|
202
|
+
if "ranked_0" in job :
|
|
203
|
+
line =f'{just_name},{row["pi_score"]},{row["iptm_ptm"]},{row["mpDockQ/pDockQ"]},{str(iQ_score)}\n'
|
|
204
|
+
mean_iQ_score[prey_name] =[]
|
|
205
|
+
mean_iQ_score[prey_name].append(iQ_score)
|
|
206
|
+
int_score[prey_name][f"iQ_score_vs_{bait_name}"] = iQ_score
|
|
207
|
+
result_dict[prey_name][f"iQ_score_vs_{bait_name}"] = iQ_score
|
|
208
|
+
if prey_name not in new_possible_prey :
|
|
209
|
+
new_possible_prey.append(prey_name)
|
|
210
|
+
if "ranked_0" in job :
|
|
211
|
+
all_lines = all_lines + line
|
|
212
|
+
for protein in possible_prey :
|
|
213
|
+
if multi_scoring == True and protein in mean_iQ_score.keys() :
|
|
214
|
+
scores = mean_iQ_score[protein]
|
|
215
|
+
mean = sum(scores) / len(scores)
|
|
216
|
+
variance = sum((x - mean) ** 2 for x in scores) / len(scores)
|
|
217
|
+
std = math.sqrt(variance)
|
|
218
|
+
cv_iQ_score[protein] = std / mean
|
|
219
|
+
mean_iQ_score[protein] = sum(scores) / len(scores)
|
|
220
|
+
if protein not in new_possible_prey :
|
|
221
|
+
int_score[protein][f"iQ_score_vs_{bait_name}"] = 0
|
|
222
|
+
result_dict[protein][f"iQ_score_vs_{bait_name}"] = 0 #if prey don't have interaction, set iQ_score to 0
|
|
223
|
+
result_dict[protein]["Reason_for_filtering"] = f"Bad interactions with {bait_name} : inter PAE > 10 A"
|
|
224
|
+
#For homo-oligomer
|
|
225
|
+
if Interaction == "homo_int" : #score homo-oligomer
|
|
226
|
+
all_homo = dict()
|
|
227
|
+
save_pi_score = dict()
|
|
228
|
+
all_lines = "jobs,pi_score,iptm_ptm,hiQ_score\n"
|
|
229
|
+
for row in reader :
|
|
230
|
+
job = row["jobs"]
|
|
231
|
+
if row["pi_score"] != 'No interface detected' :
|
|
232
|
+
if job not in all_homo.keys() :
|
|
233
|
+
all_homo[job] = (row["pi_score"],1,row)
|
|
234
|
+
save_pi_score[job] = [float(row["pi_score"])]
|
|
235
|
+
else :
|
|
236
|
+
save_pi_score[job].append(float(row["pi_score"]))
|
|
237
|
+
sum_pi_score = float(all_homo[job][0]) + float(row["pi_score"])
|
|
238
|
+
sum_int = all_homo[job][1] + 1
|
|
239
|
+
all_homo[job] = (sum_pi_score,sum_int,row)
|
|
240
|
+
for key in all_homo.keys() :
|
|
241
|
+
row = all_homo[key][2]
|
|
242
|
+
#number_oligo = row["jobs"].split("_")[2].replace("er","") #AFPD 2.0.4 #problem with "_" in name sequence ?
|
|
243
|
+
prot_name = row["jobs"].split("_homo_")[0]
|
|
244
|
+
if len(save_pi_score[key]) > int(nbr_homo) : #if model have more interface than number of homo-oligomerization
|
|
245
|
+
new_sum_pi_score = 0
|
|
246
|
+
save_pi_score[key].sort(reverse=True)
|
|
247
|
+
for index in range(0,int(nbr_homo)) :
|
|
248
|
+
new_sum_pi_score += save_pi_score[key][index]
|
|
249
|
+
hiQ_score = (((float(new_sum_pi_score)/int(nbr_homo))+2.63)/5.26)*60+float(row["iptm_ptm"])*40 #cause iptm_ptm are always same for each interface
|
|
250
|
+
line =f'{key},{str(float(new_sum_pi_score)/int(nbr_homo))},{row["iptm_ptm"]},{str(hiQ_score)}\n'
|
|
251
|
+
all_lines += line
|
|
252
|
+
else :
|
|
253
|
+
hiQ_score = (((float(all_homo[key][0])/all_homo[key][1])+2.63)/5.26)*60+float(row["iptm_ptm"])*40
|
|
254
|
+
line =f'{key},{str(float(all_homo[key][0])/all_homo[key][1])},{row["iptm_ptm"]},{str(hiQ_score)}\n'
|
|
255
|
+
all_lines += line
|
|
256
|
+
if prot_name not in new_possible_prey :
|
|
257
|
+
new_possible_prey.append(prot_name)
|
|
258
|
+
result_dict[prot_name][f"hiQ_score_{nbr_homo}er"] = hiQ_score
|
|
259
|
+
homo_score[prot_name][f"hiQ_score_{nbr_homo}er"] = hiQ_score
|
|
260
|
+
for protein in possible_prey :
|
|
261
|
+
if protein not in new_possible_prey :
|
|
262
|
+
homo_score[protein][f"hiQ_score_{nbr_homo}er"] = 0
|
|
263
|
+
result_dict[protein][f"hiQ_score_{nbr_homo}er"] = 0
|
|
264
|
+
result_dict[protein]["Reason_for_filtering"] = "Bad homo-oligomer PAE : AF"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if len(all_lines.strip("\n")) > 1 : #if all_lines is not empty
|
|
268
|
+
with open(f"result_{Interaction}/new_predictions_with_good_interpae.csv", "w") as file2 :
|
|
269
|
+
file2.write(all_lines)
|
|
270
|
+
file.set_homo_score(homo_score)
|
|
271
|
+
file.set_int_score(int_score)
|
|
272
|
+
file.set_result_dict(result_dict)
|
|
273
|
+
file.set_possible_prey(new_possible_prey)
|
|
274
|
+
else :
|
|
275
|
+
logger.info(f"result_{Interaction}/ don't exist")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def run_scoring (args) :
|
|
279
|
+
"""
|
|
280
|
+
Wrapper function executed by multiprocessing workers to score a single AlphaFold interaction using inter-chain PAE metrics.
|
|
281
|
+
|
|
282
|
+
- it unpacks arguments passed by the multiprocessing Pool
|
|
283
|
+
- calls the get_good_inter_pae scoring routine
|
|
284
|
+
- returns the resulting DataFrame
|
|
285
|
+
|
|
286
|
+
Parameters :
|
|
287
|
+
----------
|
|
288
|
+
args : tuple
|
|
289
|
+
|
|
290
|
+
Returns :
|
|
291
|
+
----------
|
|
292
|
+
result : pandas.DataFrame
|
|
293
|
+
"""
|
|
294
|
+
interaction, file, AF_version, Path_ccp4, multi_scoring = args
|
|
295
|
+
try :
|
|
296
|
+
result = HInt.get_good_inter_pae.main(interaction, 10, 2, file, AF_version, Path_ccp4, multi_scoring) #normal PAE is 10
|
|
297
|
+
return result
|
|
298
|
+
except Exception as e:
|
|
299
|
+
pid = os.getpid()
|
|
300
|
+
logger.error(f"ERROR in worker PID={pid}")
|
|
301
|
+
logger.error(f"Interaction: {interaction}")
|
|
302
|
+
raise
|
|
303
|
+
|
|
304
|
+
def Resume_file(file, Informations_dict) :
|
|
305
|
+
"""
|
|
306
|
+
Create a resume file with all interactions, their scores and their status.
|
|
307
|
+
|
|
308
|
+
- PPI interaction scores (iQ_score vs one or multiple baits)
|
|
309
|
+
- Homo-oligomerization scores (hiQ_score)
|
|
310
|
+
- Filtering reasons (OOM, size limits, etc.)
|
|
311
|
+
- Protein-level annotations (DeepLoc, signal peptide)
|
|
312
|
+
|
|
313
|
+
Two CSV files are produced:
|
|
314
|
+
------------------------
|
|
315
|
+
1. All_Final_result_HInt.csv
|
|
316
|
+
Detailed table containing score, SignalP and DeepLoc informations for each protein.
|
|
317
|
+
|
|
318
|
+
2. Summary_result_HInt.csv
|
|
319
|
+
Compact table listing only the protein name and its global status ("possible hit" or reason for filtering).
|
|
320
|
+
|
|
321
|
+
Parameters :
|
|
322
|
+
----------
|
|
323
|
+
file : object of class File_proteins
|
|
324
|
+
Informations_dict : dictionary
|
|
325
|
+
|
|
326
|
+
Returns :
|
|
327
|
+
----------
|
|
328
|
+
sorted_proteins : list of tuples
|
|
329
|
+
"""
|
|
330
|
+
logger.info("Create result table for all preys")
|
|
331
|
+
list_name_baits = list()
|
|
332
|
+
result_dict = file.get_result_dict()
|
|
333
|
+
possible_baits = Informations_dict["Multimer_bait"]
|
|
334
|
+
regions = Informations_dict["Regions"]
|
|
335
|
+
informations = ["DeepLoc","Signal_peptide"]
|
|
336
|
+
nbr_homo = Informations_dict["Homo-oligomer"]
|
|
337
|
+
big_csv_lines = "Name,Localization,Signal_peptide\n"
|
|
338
|
+
small_csv_lines = "Name,Reason_for_filtering\n"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
for protein in Informations_dict["Interact_with"] :
|
|
342
|
+
result_dict.pop(protein, None) #remove bait from result dict
|
|
343
|
+
if Informations_dict["Interact_with"] != [""] : #sorted in function of all baits
|
|
344
|
+
for multimer in possible_baits :
|
|
345
|
+
bait_name = multimer.replace(",","_and_")
|
|
346
|
+
for prot in multimer.split(",") :
|
|
347
|
+
if regions[prot] != "0-0" :
|
|
348
|
+
start = int(regions[prot].split("-")[0])
|
|
349
|
+
end = int(regions[prot].split("-")[1])
|
|
350
|
+
bait_name = bait_name.replace(prot,f"{prot}_{start}-{end}")
|
|
351
|
+
informations.append(f"iQ_score_vs_{bait_name}")
|
|
352
|
+
list_name_baits.append(bait_name)
|
|
353
|
+
big_csv_lines = big_csv_lines.strip("\n") + f",iQ_score_vs_{bait_name}\n"
|
|
354
|
+
sorted_proteins = sorted(result_dict.items(),key=lambda x: (any(f"iQ_score_vs_{bait}" in x[1] for bait in list_name_baits), sum(x[1].get(f"iQ_score_vs_{bait}", 0.0) for bait in list_name_baits)),reverse=True) #sorted in function of key of all baits iQ_score and sum of iQ_score
|
|
355
|
+
if Informations_dict["Homo-oligomer"] != "1" :
|
|
356
|
+
informations.append(f"hiQ_score_{nbr_homo}er")
|
|
357
|
+
big_csv_lines = big_csv_lines.strip("\n") + f",hiQ_score_{nbr_homo}er\n"
|
|
358
|
+
if Informations_dict["Interact_with"] == [""] and Informations_dict["Homo-oligomer"] != "1" : #if no PPI interactions filter on hiQ_score
|
|
359
|
+
sorted_proteins = sorted(result_dict.items(),key=lambda x: (x[1].get(f"hiQ_score_{nbr_homo}er", 0), len(x[1]),), reverse=True)
|
|
360
|
+
if Informations_dict["Homo-oligomer"] == "1" and Informations_dict["Interact_with"] == [""] : #no bait and no homo-oligomer
|
|
361
|
+
sorted_proteins = result_dict
|
|
362
|
+
|
|
363
|
+
sorted_dict = dict(sorted_proteins)
|
|
364
|
+
|
|
365
|
+
logger.info(sorted_dict)
|
|
366
|
+
for prot in sorted_dict.keys() :
|
|
367
|
+
small_csv_lines += prot
|
|
368
|
+
big_csv_lines += prot
|
|
369
|
+
for info in informations :
|
|
370
|
+
if info in result_dict[prot].keys() :
|
|
371
|
+
big_csv_lines += "," + str(result_dict[prot][info])
|
|
372
|
+
else :
|
|
373
|
+
big_csv_lines += ","
|
|
374
|
+
if "Reason_for_filtering" in result_dict[prot].keys() :
|
|
375
|
+
small_csv_lines += ", " + str(result_dict[prot]["Reason_for_filtering"])
|
|
376
|
+
else :
|
|
377
|
+
small_csv_lines += ", " + str("possible hit")
|
|
378
|
+
big_csv_lines += "\n"
|
|
379
|
+
small_csv_lines += "\n"
|
|
380
|
+
with open("All_Final_result_HInt.csv", "w") as All_result_file :
|
|
381
|
+
All_result_file.write(big_csv_lines)
|
|
382
|
+
with open("./log_file/Summary_result_HInt.csv", "w") as summary :
|
|
383
|
+
summary.write(small_csv_lines)
|
|
384
|
+
return sorted_proteins
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
#Generate figures
|
|
388
|
+
def Create_figures (file, Informations_dict, AF_version, sorted_proteins, CPU) :
|
|
389
|
+
"""
|
|
390
|
+
Generate structural and sequence-level figures for validated prey proteins, parrallelized across multiple CPU cores.
|
|
391
|
+
|
|
392
|
+
This function produces figures only for :
|
|
393
|
+
- Monomeric baits (single protein only)
|
|
394
|
+
- Preys that passed all filtering steps (i.e. no "Reason_for_filtering")
|
|
395
|
+
|
|
396
|
+
- Distogram (AlphaFold v2 only)
|
|
397
|
+
- Colored PDB structures highlighting interface residues
|
|
398
|
+
- Interface residue tables
|
|
399
|
+
- Clustering of interfaces across ranked proteins
|
|
400
|
+
- Sequence-level interface visualization
|
|
401
|
+
|
|
402
|
+
Parameters :
|
|
403
|
+
----------
|
|
404
|
+
file : object of class File_proteins
|
|
405
|
+
Informations_dict : dict
|
|
406
|
+
AF_version : str
|
|
407
|
+
sorted_proteins : list
|
|
408
|
+
CPU : int
|
|
409
|
+
|
|
410
|
+
Returns :
|
|
411
|
+
----------
|
|
412
|
+
tasks : list of tuples
|
|
413
|
+
"""
|
|
414
|
+
logger.info("Create figures for all validate preys")
|
|
415
|
+
regions = Informations_dict["Regions"]
|
|
416
|
+
possible_prey = file.get_possible_prey()
|
|
417
|
+
result_dict = file.get_result_dict()
|
|
418
|
+
complete_lenght_prot = file.get_lenght_prot()
|
|
419
|
+
complete_seq_prot = file.get_proteins_sequence_no_SP()
|
|
420
|
+
|
|
421
|
+
interface_dict = dict()
|
|
422
|
+
tasks = []
|
|
423
|
+
baits_seq = {}
|
|
424
|
+
baits_lenght = {}
|
|
425
|
+
for baits in Informations_dict["Multimer_bait"] :
|
|
426
|
+
bait_file = baits
|
|
427
|
+
for bait in baits.split(",") :
|
|
428
|
+
if regions[bait] != "0-0" :
|
|
429
|
+
start = int(regions[bait].split("-")[0])
|
|
430
|
+
end = int(regions[bait].split("-")[1])
|
|
431
|
+
bait_file = bait_file.replace(bait,f"{bait}_{start}-{end}")
|
|
432
|
+
baits_seq [bait] = complete_seq_prot[bait]
|
|
433
|
+
baits_lenght [bait] = complete_lenght_prot[bait]
|
|
434
|
+
bait_file = bait_file.replace(",","_and_")
|
|
435
|
+
for prey in possible_prey :
|
|
436
|
+
lenght_prot = copy.deepcopy(baits_lenght)
|
|
437
|
+
lenght_prot [prey] = complete_lenght_prot[prey]
|
|
438
|
+
seq_prot = copy.deepcopy(baits_seq)
|
|
439
|
+
seq_prot [prey] = complete_seq_prot[prey]
|
|
440
|
+
if "Reason_for_filtering" not in result_dict[prey].keys() : #only for validate preys
|
|
441
|
+
tasks.append((AF_version, bait_file, prey, lenght_prot, seq_prot, baits, regions))
|
|
442
|
+
if tasks : #if there is interaction to process
|
|
443
|
+
with Pool(processes=CPU) as pool :
|
|
444
|
+
results_res_int = pool.map(postprocess_interaction, tasks)
|
|
445
|
+
for d in results_res_int :
|
|
446
|
+
for key, list_int in d.items() :
|
|
447
|
+
if key in interface_dict :
|
|
448
|
+
interface_dict[key] += list_int
|
|
449
|
+
else :
|
|
450
|
+
interface_dict[key] = list_int.copy()
|
|
451
|
+
|
|
452
|
+
interface_dict = cluster_interface(interface_dict, sorted_proteins)
|
|
453
|
+
plot_sequence_interface(file, interface_dict)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def postprocess_interaction (args) : #maybe split first and second part of function
|
|
459
|
+
"""
|
|
460
|
+
Post-process a single AlphaFold interaction to generate structural figures and interface residue tables.
|
|
461
|
+
|
|
462
|
+
Parameters :
|
|
463
|
+
----------
|
|
464
|
+
args : tuple
|
|
465
|
+
|
|
466
|
+
Returns :
|
|
467
|
+
----------
|
|
468
|
+
interface_dict : dict
|
|
469
|
+
"""
|
|
470
|
+
(AF_version, bait_file, prey, lenght_prot, seq_prot, baits, region) = args
|
|
471
|
+
if os.path.isdir (f"./result_PPI_int/{bait_file}_and_{prey}") == True :
|
|
472
|
+
outdir = f"./result_PPI_int/{bait_file}_and_{prey}"
|
|
473
|
+
if os.path.isdir (f"./result_PPI_int/{prey}_and_{bait_file}") == True :
|
|
474
|
+
outdir = f"./result_PPI_int/{prey}_and_{bait_file}"
|
|
475
|
+
interface_dict = dict()
|
|
476
|
+
if AF_version == "2" :
|
|
477
|
+
plot_Distogram(outdir)
|
|
478
|
+
|
|
479
|
+
residues_at_interface, proteins, path_int, color_res = make_table_res_int(lenght_prot, seq_prot, outdir, baits, prey, AF_version, region)
|
|
480
|
+
|
|
481
|
+
if residues_at_interface is not None :
|
|
482
|
+
color_int_residues(path_int, color_res, proteins)
|
|
483
|
+
interface_dict = define_interface(residues_at_interface)
|
|
484
|
+
|
|
485
|
+
return interface_dict
|
|
486
|
+
|
|
487
|
+
def plot_Distogram (job) :
|
|
488
|
+
"""
|
|
489
|
+
Generate a distance map (distogram) for the best model of a given AlphaFold job.
|
|
490
|
+
|
|
491
|
+
Only generates the distogram if it does not already exist as a PNG.
|
|
492
|
+
Works with both pickled (.pkl) and gzipped (.pkl.gz) result files.
|
|
493
|
+
|
|
494
|
+
Parameters :
|
|
495
|
+
----------
|
|
496
|
+
job : str
|
|
497
|
+
"""
|
|
498
|
+
ranking_results = json.load(open(os.path.join(f'{job}/ranking_debug.json')))
|
|
499
|
+
best_model = ranking_results["order"][0]
|
|
500
|
+
del ranking_results
|
|
501
|
+
gc.collect()
|
|
502
|
+
out_png = f"{job}/result_{best_model}.dmap.png"
|
|
503
|
+
if os.path.exists(f'{job}/result_{best_model}.dmap.png') == False :
|
|
504
|
+
if os.path.isfile(f'{job}/result_{best_model}.pkl.gz') :
|
|
505
|
+
path_file = f'{job}/result_{best_model}.pkl.gz'
|
|
506
|
+
if os.path.isfile(f'{job}/result_{best_model}.pkl') :
|
|
507
|
+
path_file = f'{job}/result_{best_model}.pkl'
|
|
508
|
+
if path_file.endswith(".gz") :
|
|
509
|
+
with gzip.open(path_file, "rb") as f :
|
|
510
|
+
results = pickle.load(f)
|
|
511
|
+
else :
|
|
512
|
+
with open(path_file, "rb") as f :
|
|
513
|
+
results = pickle.load(f)
|
|
514
|
+
dist = None
|
|
515
|
+
if "distogram" in results.keys() : #avoid error from APD release
|
|
516
|
+
|
|
517
|
+
bin_edges = np.insert(results["distogram"]["bin_edges"], 0, 0)
|
|
518
|
+
|
|
519
|
+
logits = results["distogram"]["logits"] # alias (avoid deep copy)
|
|
520
|
+
lengths = [len(s) for s in results.get("seqs", [])]
|
|
521
|
+
del results
|
|
522
|
+
gc.collect()
|
|
523
|
+
|
|
524
|
+
logits = logits - np.max(logits, axis=2, keepdims=True)
|
|
525
|
+
exp_logits = np.exp(logits)
|
|
526
|
+
probs = exp_logits / np.sum(exp_logits, axis=2, keepdims=True)
|
|
527
|
+
|
|
528
|
+
dist = np.tensordot(probs, bin_edges, axes=([2], [0]))
|
|
529
|
+
del logits, exp_logits, probs, bin_edges
|
|
530
|
+
gc.collect()
|
|
531
|
+
|
|
532
|
+
fig, ax = plt.subplots()
|
|
533
|
+
|
|
534
|
+
im = ax.imshow(dist, interpolation="nearest")
|
|
535
|
+
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
536
|
+
|
|
537
|
+
ax.set_title("Distance map")
|
|
538
|
+
|
|
539
|
+
pos = 0
|
|
540
|
+
for L in lengths[:-1]:
|
|
541
|
+
pos += L
|
|
542
|
+
ax.axhline(pos, color="black", linewidth=1)
|
|
543
|
+
ax.axvline(pos, color="black", linewidth=1)
|
|
544
|
+
|
|
545
|
+
fig.savefig(out_png, dpi=300, bbox_inches="tight")
|
|
546
|
+
plt.close(fig)
|
|
547
|
+
|
|
548
|
+
del dist
|
|
549
|
+
gc.collect()
|
|
550
|
+
logger.info(f"Distogram created for {job}")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def make_table_res_int (lenght_prot, seq_prot, path_int, baits, prey, AF_version, regions) :
|
|
554
|
+
"""
|
|
555
|
+
Generate a detailed table of residue-residue interactions for a protein-protein complex.
|
|
556
|
+
|
|
557
|
+
This function analyzes the structural model of a protein complex to identify residues at the interface between the bait and prey proteins. It considers both AlphaFold2 and
|
|
558
|
+
AlphaFold3 outputs, using distance thresholds and predicted aligned error (PAE) to filter meaningful interactions. The function also prepares color-coding for interface
|
|
559
|
+
residues for downstream visualization.
|
|
560
|
+
|
|
561
|
+
Parameters :
|
|
562
|
+
----------
|
|
563
|
+
lenght_prot : dict
|
|
564
|
+
seq_prot : dict
|
|
565
|
+
path_int : str
|
|
566
|
+
baits : str
|
|
567
|
+
prey : str
|
|
568
|
+
AF_verison : str
|
|
569
|
+
regions : dict
|
|
570
|
+
|
|
571
|
+
Returns :
|
|
572
|
+
----------
|
|
573
|
+
residues_at_interface : list of lists or None
|
|
574
|
+
proteins : list of str
|
|
575
|
+
path_int : str
|
|
576
|
+
color_res : dict
|
|
577
|
+
|
|
578
|
+
Notes :
|
|
579
|
+
----------
|
|
580
|
+
- For AlphaFold2, interactions are extracted from the predicted aligned error (PAE) matrix and the distogram. Filtered by a distance cutoff (10 Å) and PAE threshold (10 Å).
|
|
581
|
+
- For AlphaFold3, interactions are extracted directly from the atomic coordinates in the PDB file, filtered by a distance cutoff (10 Å) and PAE threshold (10 Å). Only consider standard backbone and Cβ atoms for distance calculations.
|
|
582
|
+
"""
|
|
583
|
+
parser = PDB.PDBParser(QUIET=True)
|
|
584
|
+
names_int = path_int.split('/')[2]
|
|
585
|
+
dict_int = dict()
|
|
586
|
+
proteins = [bait for bait in baits.split(",")]
|
|
587
|
+
proteins.append(prey)
|
|
588
|
+
color_res = dict()
|
|
589
|
+
dist_k = True
|
|
590
|
+
for prot in proteins :
|
|
591
|
+
color_res[prot] = set()
|
|
592
|
+
if AF_version == "2" :
|
|
593
|
+
ranking_results = json.load(open(os.path.join(f'{path_int}/ranking_debug.json')))
|
|
594
|
+
best_model = ranking_results["order"][0]
|
|
595
|
+
if os.path.isfile(f'{path_int}/result_{best_model}.pkl.gz') :
|
|
596
|
+
path_file = f'{path_int}/result_{best_model}.pkl.gz'
|
|
597
|
+
if os.path.isfile(f'{path_int}/result_{best_model}.pkl') :
|
|
598
|
+
path_file = f'{path_int}/result_{best_model}.pkl'
|
|
599
|
+
with open(os.path.join(path_file), 'rb') as inf_file :
|
|
600
|
+
if ".gz" in path_file :
|
|
601
|
+
pickle_dict = pickle.load(gzip.open(inf_file))
|
|
602
|
+
else :
|
|
603
|
+
pickle_dict = pickle.load(inf_file)
|
|
604
|
+
|
|
605
|
+
if "distogram" not in pickle_dict.keys() :
|
|
606
|
+
dist_k = False
|
|
607
|
+
else :
|
|
608
|
+
bin_edges = np.insert(pickle_dict["distogram"]["bin_edges"], 0, 0) #take distogram for distance
|
|
609
|
+
logits = pickle_dict["distogram"]["logits"]
|
|
610
|
+
dist = bin_edges[np.argmax(logits, axis=2)]
|
|
611
|
+
pae_mtx = pickle_dict["predicted_aligned_error"]#take PAE
|
|
612
|
+
del pickle_dict
|
|
613
|
+
del logits
|
|
614
|
+
del bin_edges
|
|
615
|
+
gc.collect()
|
|
616
|
+
complete_lenght = 0
|
|
617
|
+
max_hori_index = 0
|
|
618
|
+
for bait in baits.split(",") :
|
|
619
|
+
complete_lenght += lenght_prot[bait]
|
|
620
|
+
for bait in baits.split(",") :
|
|
621
|
+
min_hori_index = max_hori_index
|
|
622
|
+
max_hori_index += lenght_prot[bait]
|
|
623
|
+
bait_prey = bait +"_and_" + proteins[-1]
|
|
624
|
+
dict_int[bait_prey] = [[bait," "+proteins[-1]," Distance_Ä"," PAE_score"]]
|
|
625
|
+
for line in range(complete_lenght,complete_lenght+lenght_prot[proteins[-1]]) :
|
|
626
|
+
hori_index = -1
|
|
627
|
+
for distance in dist[line] :
|
|
628
|
+
hori_index += 1
|
|
629
|
+
if hori_index < max_hori_index and hori_index >= min_hori_index :
|
|
630
|
+
if distance <= 10 : #center of mass of the residue
|
|
631
|
+
if pae_mtx[line][hori_index] <= 10 :
|
|
632
|
+
real_hori_index = hori_index - min_hori_index
|
|
633
|
+
res_in_tot_seq = real_hori_index
|
|
634
|
+
if regions[bait] != "0-0" : #if region selected, need to ajust index
|
|
635
|
+
res_in_tot_seq = hori_index - min_hori_index + int(regions[bait].split("-")[0]) - 1
|
|
636
|
+
residue1 = seq_prot[bait][res_in_tot_seq]
|
|
637
|
+
residue2 = seq_prot[proteins[-1]][line-complete_lenght]
|
|
638
|
+
dict_int[bait_prey].append([residue1+":"+str(res_in_tot_seq+1)," "+residue2+":"+str(line-complete_lenght+1)," "+str(distance), " "+str(pae_mtx[line][real_hori_index])])
|
|
639
|
+
color_res[bait].add(str(res_in_tot_seq+1))
|
|
640
|
+
color_res[proteins[-1]].add(str(line-complete_lenght+1))
|
|
641
|
+
del dist
|
|
642
|
+
del pae_mtx
|
|
643
|
+
gc.collect()
|
|
644
|
+
if AF_version == "3" or dist_k == False : #if no distogram, use only PAE and distance from pdb
|
|
645
|
+
if os.path.isfile(f'{path_int}/{path_int.split("/")[-1]}_confidences.json') == True :
|
|
646
|
+
conf_file = f'{path_int.split("/")[-1]}_confidences.json'
|
|
647
|
+
else :
|
|
648
|
+
conf_file = 'ranked_0_confidences.json'
|
|
649
|
+
with open(os.path.join(path_int, conf_file), 'rb') as json_f :
|
|
650
|
+
pae_mtx = np.array(json.load(json_f)['pae'])
|
|
651
|
+
DIST_CUTOFF = 10.0 # Å (CA/CB/C)
|
|
652
|
+
PAE_CUTOFF = 10.0 #Observation: PAE value for residue at the interaciotn of AF3 model is generally lower than AF2 model
|
|
653
|
+
ATOM_CONTACT = ["C","CA","CB"]
|
|
654
|
+
|
|
655
|
+
len_chain_last = lenght_prot[proteins[-1]]
|
|
656
|
+
total_len = pae_mtx.shape[0]
|
|
657
|
+
int_already_know = {}
|
|
658
|
+
structure = parser.get_structure('protein',os.path.join(path_int, f"{names_int}_ranked_0.pdb"))
|
|
659
|
+
for model in structure :
|
|
660
|
+
chains = model.get_list()
|
|
661
|
+
last_chain = chains[-1]
|
|
662
|
+
baits = baits.split(",")
|
|
663
|
+
for i, chain1 in enumerate(chains[:-1]) :
|
|
664
|
+
chain2 = last_chain
|
|
665
|
+
interaction = baits[i] +"_and_"+ proteins[-1]
|
|
666
|
+
|
|
667
|
+
if interaction not in dict_int :
|
|
668
|
+
dict_int[interaction] = [[baits[i], " " + proteins[-1], " Distance_Å", " PAE_score"]]
|
|
669
|
+
for res1 in chain1:
|
|
670
|
+
if res1.id[0] != " " :
|
|
671
|
+
continue
|
|
672
|
+
for res2 in chain2:
|
|
673
|
+
if res2.id[0] != " " :
|
|
674
|
+
continue
|
|
675
|
+
for atom1 in res1:
|
|
676
|
+
if atom1.get_id() not in ATOM_CONTACT:
|
|
677
|
+
continue
|
|
678
|
+
for atom2 in res2 :
|
|
679
|
+
if atom2.get_id() not in ATOM_CONTACT:
|
|
680
|
+
continue
|
|
681
|
+
dist = atom1 - atom2
|
|
682
|
+
if dist > DIST_CUTOFF:
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
r1 = res1.id[1] - 1
|
|
686
|
+
r2 = res2.id[1] - 1
|
|
687
|
+
|
|
688
|
+
idx1 = r1
|
|
689
|
+
idx2 = total_len - len_chain_last + r2
|
|
690
|
+
|
|
691
|
+
if idx1 >= total_len or idx2 >= total_len:
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
pae_score = float(pae_mtx[idx1, idx2])
|
|
695
|
+
|
|
696
|
+
if pae_score > PAE_CUTOFF :
|
|
697
|
+
continue
|
|
698
|
+
|
|
699
|
+
key = (f"{chain1.get_id()}:{res1.get_resname()} {res1.id[1]}", f"{chain2.get_id()}:{res2.get_resname()} {res2.id[1]}")
|
|
700
|
+
color_res[baits[i]].add(str(res1.id[1]))
|
|
701
|
+
color_res[proteins[-1]].add(str(res2.id[1]))
|
|
702
|
+
|
|
703
|
+
if key in int_already_know:
|
|
704
|
+
if dist < int_already_know[key][0] :
|
|
705
|
+
int_already_know[key] = (dist, pae_score)
|
|
706
|
+
else:
|
|
707
|
+
int_already_know[key] = (dist, pae_score)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
for (resA, resB), (dist, pae) in int_already_know.items() :
|
|
711
|
+
dict_int[interaction].append([f"{resA[2:5]}:{resA.split()[-1]}",f" {resB[2:5]}:{resB.split()[-1]}",f" {dist:.2f}",f" {pae:.2f}"])
|
|
712
|
+
del structure
|
|
713
|
+
del pae_mtx
|
|
714
|
+
gc.collect()
|
|
715
|
+
residues_at_interface = dict()
|
|
716
|
+
for chains in dict_int.keys() :
|
|
717
|
+
residues_at_interface[chains] = []
|
|
718
|
+
fileout = chains+"_res_int.csv"
|
|
719
|
+
np_table = np.array(dict_int[chains])
|
|
720
|
+
with open(f"{path_int}/"+fileout, "w", newline="") as csv_table :
|
|
721
|
+
mywriter = csv.writer(csv_table, delimiter=",")
|
|
722
|
+
mywriter.writerows(np_table)
|
|
723
|
+
del dict_int[chains][0] #delete title of each col
|
|
724
|
+
for interaction in dict_int[chains] :
|
|
725
|
+
if interaction not in residues_at_interface[chains] :
|
|
726
|
+
residues_at_interface[chains].append(interaction)
|
|
727
|
+
if residues_at_interface != dict() : #can arrive if it don't find atom with distance < 10 or PAE < 10
|
|
728
|
+
return residues_at_interface,proteins,path_int,color_res
|
|
729
|
+
else :
|
|
730
|
+
return None,None,None,None
|
|
731
|
+
|
|
732
|
+
def color_int_residues(pdb_path, residues_to_color, names) :
|
|
733
|
+
"""
|
|
734
|
+
Color residues involved in protein-protein interactions in a PDB file.
|
|
735
|
+
|
|
736
|
+
This function modifies the B-factor (temperature factor) column of a PDB file to indicate interface residues.
|
|
737
|
+
|
|
738
|
+
Parameters :
|
|
739
|
+
----------
|
|
740
|
+
pdb_path : str
|
|
741
|
+
residues_to_color : dict
|
|
742
|
+
names : str
|
|
743
|
+
"""
|
|
744
|
+
names_int = pdb_path.split('/')[2]
|
|
745
|
+
save_lines = list()
|
|
746
|
+
chain_to_prot = dict()
|
|
747
|
+
chain_index = 0
|
|
748
|
+
with open(f"{pdb_path}/{names_int}_ranked_0.pdb", "r") as file_in :
|
|
749
|
+
for line in file_in:
|
|
750
|
+
if line.startswith("ATOM") :
|
|
751
|
+
chain = line[21]
|
|
752
|
+
if chain not in chain_to_prot :
|
|
753
|
+
chain_to_prot[chain] = names[chain_index]
|
|
754
|
+
chain_index += 1
|
|
755
|
+
|
|
756
|
+
prot = chain_to_prot[chain]
|
|
757
|
+
res_num = line[22:26].strip()
|
|
758
|
+
|
|
759
|
+
if res_num in residues_to_color.get(prot, set()) :
|
|
760
|
+
line = line[:60] + "100.00" + line[66:]
|
|
761
|
+
else:
|
|
762
|
+
line = line[:60] + " 0.00" + line[66:]
|
|
763
|
+
|
|
764
|
+
save_lines.append(line)
|
|
765
|
+
|
|
766
|
+
with open(f"{pdb_path}/{names_int}_ranked_0.pdb", "w") as writer:
|
|
767
|
+
writer.writelines(save_lines)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def define_interface (residues_at_interface) :
|
|
771
|
+
"""
|
|
772
|
+
Create a dictionary of interacting residues for a protein pair.
|
|
773
|
+
|
|
774
|
+
This function parses a list of interacting residue pairs and adds them to the existing interface dictionary. Each protein in the pair gets a list of residues
|
|
775
|
+
that interact with the other protein. The second protein's name is also added to the list for reference.
|
|
776
|
+
|
|
777
|
+
Parameters :
|
|
778
|
+
----------
|
|
779
|
+
residues_at_interface : dict of lists
|
|
780
|
+
|
|
781
|
+
Returns :
|
|
782
|
+
----------
|
|
783
|
+
old_interface_dict : dict
|
|
784
|
+
"""
|
|
785
|
+
old_interface_dict = {}
|
|
786
|
+
for inter in residues_at_interface.keys() :
|
|
787
|
+
all_residues_int = residues_at_interface[inter]
|
|
788
|
+
if residues_at_interface[inter] == [] :
|
|
789
|
+
continue
|
|
790
|
+
protein1 = inter.split("_and_")[0]
|
|
791
|
+
protein2 = inter.split("_and_")[1]
|
|
792
|
+
list_int_protein1 = list()
|
|
793
|
+
list_int_protein2 = list()
|
|
794
|
+
if protein1 not in old_interface_dict.keys() :
|
|
795
|
+
old_interface_dict[protein1] = []
|
|
796
|
+
if protein2 not in old_interface_dict.keys() :
|
|
797
|
+
old_interface_dict[protein2] = []
|
|
798
|
+
for line in all_residues_int :
|
|
799
|
+
line[1] = line[1].strip()
|
|
800
|
+
if line[0] != inter[0] and "chain" not in line[0] :
|
|
801
|
+
if line[0].split(":")[1] not in list_int_protein1 :
|
|
802
|
+
list_int_protein1.append(line[0].split(":")[1])
|
|
803
|
+
if line[1].split(":")[1] not in list_int_protein2 :
|
|
804
|
+
list_int_protein2.append(line[1].split(":")[1])
|
|
805
|
+
list_int_protein1.append(protein2) #last values of each list is the second proteins
|
|
806
|
+
list_int_protein2.append(protein1)
|
|
807
|
+
old_interface_dict[protein1].append(list_int_protein1)
|
|
808
|
+
old_interface_dict[protein2].append(list_int_protein2)
|
|
809
|
+
|
|
810
|
+
return old_interface_dict
|
|
811
|
+
|
|
812
|
+
def cluster_interface (interface_dict, sorted_proteins) :
|
|
813
|
+
"""
|
|
814
|
+
Cluster and classify protein interfaces based on residue overlap.
|
|
815
|
+
|
|
816
|
+
This function analyzes all detected interaction interfaces for each protein and assigns a letter (a-z) to represent unique interfaces. Interfaces with significant
|
|
817
|
+
overlap (Jaccard similarity ≥ 0.20) are considered the same and share the same letter. Small interfaces or those with low similarity get a new letter. The function also
|
|
818
|
+
limits the number of interfaces per protein to 27 betters.
|
|
819
|
+
|
|
820
|
+
Parameters :
|
|
821
|
+
----------
|
|
822
|
+
interface_dict : dict
|
|
823
|
+
sorted_proteins : dict
|
|
824
|
+
|
|
825
|
+
Returns :
|
|
826
|
+
----------
|
|
827
|
+
interface_dict : dict
|
|
828
|
+
Updated dictionary where each interface list starts with a letter (a-z) representing the interface cluster. Interfaces with similar residues share the same letter.
|
|
829
|
+
"""
|
|
830
|
+
alphabet = list(string.ascii_lowercase) + [c*2 for c in string.ascii_lowercase]
|
|
831
|
+
for proteins in interface_dict.keys() :
|
|
832
|
+
if len(interface_dict[proteins]) >= 27 : #Limit to 27 interfaces per protein
|
|
833
|
+
best_preys = list()
|
|
834
|
+
for prey in sorted_proteins :
|
|
835
|
+
best_preys.append(prey[0])
|
|
836
|
+
if len(best_preys) >= 27 :
|
|
837
|
+
break
|
|
838
|
+
copy_interface = copy.deepcopy(interface_dict[proteins])
|
|
839
|
+
for interface in copy_interface :
|
|
840
|
+
if interface[len(interface)-1] not in best_preys :
|
|
841
|
+
interface_dict[proteins].remove(interface)
|
|
842
|
+
alpha_index = 0
|
|
843
|
+
already_inter = list()
|
|
844
|
+
interface_dict[proteins] = sorted(interface_dict[proteins], key=lambda x : len(x)) #sorted all interface in function of number of residues
|
|
845
|
+
for interface1 in range(len(interface_dict[proteins])) :
|
|
846
|
+
if interface1 == 0 : #if it's the first interface, define a
|
|
847
|
+
interface_dict[proteins][interface1].insert(0,alphabet[0])
|
|
848
|
+
already_inter.append(alphabet[0])
|
|
849
|
+
for interface2 in range(interface1+1,len(interface_dict[proteins])) :
|
|
850
|
+
alpha_index += 1
|
|
851
|
+
list_inter = list(set(interface_dict[proteins][interface1]).intersection(set(interface_dict[proteins][interface2])))
|
|
852
|
+
simi_inter = len(list_inter)/(len(set(interface_dict[proteins][interface1]).union(set(interface_dict[proteins][interface2])))-3) #indice jaccard # -3 just to remove interface 'a' and uniprotID from .union()
|
|
853
|
+
if simi_inter < 0.20: #create a new interface #Bias on small interface
|
|
854
|
+
if interface_dict[proteins][interface2][0] in already_inter : #Don't create new interface if it already has one
|
|
855
|
+
pass
|
|
856
|
+
else :
|
|
857
|
+
interface_dict[proteins][interface2].insert(0,alphabet[alpha_index])
|
|
858
|
+
already_inter.append(alphabet[alpha_index])
|
|
859
|
+
else : #if interfaces got more than 0.20 of same residues, it's the same interface
|
|
860
|
+
if interface_dict[proteins][interface2][0] in alphabet :
|
|
861
|
+
interface_dict[proteins][interface2].pop(0)
|
|
862
|
+
interface_dict[proteins][interface2].insert(0,interface_dict[proteins][interface1][0]) #set same letter for same interface
|
|
863
|
+
alpha_index -= 1
|
|
864
|
+
return interface_dict
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def plot_sequence_interface (file, dict_inter) :
|
|
869
|
+
"""
|
|
870
|
+
Generate sequence-based interface diagrams for proteins.
|
|
871
|
+
|
|
872
|
+
This function visualizes interacting residues along a protein sequence. Residues involved in different interfaces are color-coded, and residues
|
|
873
|
+
participating in multiple interfaces can show multiple colors. The output is a PNG figure for each protein, showing the sequence with interface annotations.
|
|
874
|
+
|
|
875
|
+
Parameters :
|
|
876
|
+
----------
|
|
877
|
+
file : object of File_proteins class
|
|
878
|
+
dict_inter : dict
|
|
879
|
+
|
|
880
|
+
Notes :
|
|
881
|
+
----------
|
|
882
|
+
- Residues involved in multiple interfaces are displayed with stacked color blocks.
|
|
883
|
+
- Each line of the figure contains up to 150 residues (adjustable by `line_adjust`).
|
|
884
|
+
"""
|
|
885
|
+
if not os.path.exists("./Interface_fig/") :
|
|
886
|
+
os.makedirs("./Interface_fig/")
|
|
887
|
+
sequence_dict = file.get_proteins_sequence_no_SP()
|
|
888
|
+
all_color = ['red','green', 'blue', 'orange', 'purple', 'cyan', 'magenta', 'yellow', 'pink', 'brown','lime', 'indigo', 'violet', 'turquoise', 'teal', 'crimson', 'gold', 'salmon', 'plum', 'chartreuse']
|
|
889
|
+
for uniprotID_main in dict_inter.keys() :
|
|
890
|
+
sequence = sequence_dict[uniprotID_main]
|
|
891
|
+
indice_color = -1
|
|
892
|
+
interface_done = dict()
|
|
893
|
+
index_to_color = dict()
|
|
894
|
+
uniprot_id_interface = dict()
|
|
895
|
+
for interaction in dict_inter[uniprotID_main] : #list of residue + interface + UniprotID in interaction
|
|
896
|
+
if interaction[0] not in interface_done.keys() : #if it's a new interface
|
|
897
|
+
indice_color += 1
|
|
898
|
+
interface_done[interaction[0]] = all_color[indice_color]
|
|
899
|
+
uniprot_id_interface[interaction[len(interaction)-1]] = all_color[indice_color]
|
|
900
|
+
for aa_to_color in interaction :
|
|
901
|
+
if " " in aa_to_color :
|
|
902
|
+
if aa_to_color.split(" ")[1] not in index_to_color.keys() :
|
|
903
|
+
index_to_color[aa_to_color.split(" ")[1]] = [all_color[indice_color]]
|
|
904
|
+
if aa_to_color.split(" ")[1] in index_to_color.keys() and all_color[indice_color] not in index_to_color[aa_to_color.split(" ")[1]] : #add two colour if it's in two interface
|
|
905
|
+
index_to_color[aa_to_color.split(" ")[1]].append(all_color[indice_color])
|
|
906
|
+
else : #for seconde residue table
|
|
907
|
+
if aa_to_color not in index_to_color.keys() :
|
|
908
|
+
index_to_color[aa_to_color] = [all_color[indice_color]]
|
|
909
|
+
if aa_to_color in index_to_color.keys() and all_color[indice_color] not in index_to_color[aa_to_color] : #add two colour if it's in two interface
|
|
910
|
+
index_to_color[aa_to_color].append(all_color[indice_color])
|
|
911
|
+
else :
|
|
912
|
+
uniprot_id_interface[interaction[len(interaction)-1]] = interface_done[interaction[0]]
|
|
913
|
+
for aa_to_color in interaction :
|
|
914
|
+
if " " in aa_to_color :
|
|
915
|
+
if aa_to_color.split(" ")[1] not in index_to_color.keys() :
|
|
916
|
+
index_to_color[aa_to_color.split(" ")[1]] = [all_color[indice_color]]
|
|
917
|
+
if aa_to_color.split(" ")[1] in index_to_color.keys() and all_color[indice_color] not in index_to_color[aa_to_color.split(" ")[1]] : #add two colour if it's in two interface
|
|
918
|
+
index_to_color[aa_to_color.split(" ")[1]].append(all_color[indice_color])
|
|
919
|
+
else : #for seconde residue table
|
|
920
|
+
if aa_to_color not in index_to_color.keys() :
|
|
921
|
+
index_to_color[aa_to_color] = [interface_done[interaction[0]]]
|
|
922
|
+
if aa_to_color in index_to_color.keys() and interface_done[interaction[0]] not in index_to_color[aa_to_color] : #add two colour if it's in two interface
|
|
923
|
+
index_to_color[aa_to_color].append(interface_done[interaction[0]])
|
|
924
|
+
line_adjust = 150 #max aa per line
|
|
925
|
+
dict_name = dict()
|
|
926
|
+
n_lines = (len(sequence) + line_adjust - 1) // line_adjust
|
|
927
|
+
fig, ax = plt.subplots(figsize=(line_adjust / 4, n_lines*1.5)) #Adjust figsize
|
|
928
|
+
for line_index in range(0, len(sequence), line_adjust) :
|
|
929
|
+
sub_sequence = sequence[line_index:line_index + line_adjust]
|
|
930
|
+
y_pos = -line_index // line_adjust * 1.5
|
|
931
|
+
for i in range(len(sub_sequence)) :
|
|
932
|
+
aa = sub_sequence[i]
|
|
933
|
+
total_index = line_index + i
|
|
934
|
+
if str(total_index + 1) in index_to_color.keys() :
|
|
935
|
+
colors = index_to_color[str(total_index + 1)]
|
|
936
|
+
height = 0.5 / len(colors)
|
|
937
|
+
for color_index, color in enumerate(colors) :
|
|
938
|
+
ax.add_patch(plt.Rectangle((i, y_pos + color_index * height), 1, height, color=color))
|
|
939
|
+
ax.text(i + 0.5, y_pos + 0.25, aa, ha='center', va='center', color='white')
|
|
940
|
+
else :
|
|
941
|
+
ax.add_patch(plt.Rectangle((i, y_pos), 1, 0.6, color="white"))
|
|
942
|
+
ax.text(i + 0.5, y_pos + 0.25, aa, ha='center', va='center', color='black')
|
|
943
|
+
if (total_index+1) % 10 == 0 or i == 0 :
|
|
944
|
+
ax.text(i + 0.5, y_pos + 0.5, str(total_index + 1), ha='center', va='center', color='black', fontsize=7)
|
|
945
|
+
for index_neigh, neigh in enumerate(uniprot_id_interface) :
|
|
946
|
+
name_neigh = f"{neigh}({dict_name[neigh]})" if neigh in dict_name else neigh
|
|
947
|
+
ax.text(index_neigh * 6, - n_lines * 2, name_neigh, ha='center', va='center', color=uniprot_id_interface[neigh], fontsize=8)
|
|
948
|
+
uniprotID_main_name = f"{uniprotID_main}({dict_name[uniprotID_main]})" if uniprotID_main in dict_name else uniprotID_main
|
|
949
|
+
ax.text(-2, 0.25, uniprotID_main_name, ha='right', va='center', color='black', fontsize=10, fontweight='bold')
|
|
950
|
+
ax.set_xlim(0, line_adjust)
|
|
951
|
+
ax.set_ylim(-n_lines*2, 1) #Adjust high
|
|
952
|
+
ax.axis('off')
|
|
953
|
+
plt.savefig("./Interface_fig/"+uniprotID_main+"_interface_fig.png", dpi=300, bbox_inches='tight')
|
|
954
|
+
plt.close()
|