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.
@@ -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()