Oncodrive3D 1.0.4__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,900 @@
1
+ """
2
+ Module to generate a pandas dataframe including identifiers mapped to protein and DNA sequences.
3
+
4
+ The functions are used to extract all protein sequences of PDB structures in a given directory;
5
+ use EMBOSS backtranseq back translate proteins sequences into DNA; generate a dataframe including HUGO symbol,
6
+ Uniprot_ID, protein, and DNA sequences. This dataframe is required to get the probability of each residue to mutate
7
+ (missense mutation) based on the mutation profile (mutation rate in 96 trinucleotide contexts) of the cohort.
8
+ The per-residue missense mutation probability of each protein is then used to get the probability of a
9
+ certain volume to be hit by a missense mutation.
10
+ """
11
+
12
+
13
+ import ast
14
+ import json
15
+ import multiprocessing
16
+ import os
17
+ import re
18
+ import shlex
19
+ import subprocess
20
+ import sys
21
+ import time
22
+
23
+ import daiquiri
24
+ import numpy as np
25
+ import pandas as pd
26
+ import requests
27
+ from bgreference import hg38, mm39
28
+ from Bio.Seq import Seq
29
+ from tqdm import tqdm
30
+
31
+ from scripts import __logger_name__
32
+ from scripts.datasets.utils import (
33
+ download_single_file,
34
+ get_af_id_from_pdb,
35
+ get_pdb_path_list_from_dir,
36
+ get_seq_from_pdb,
37
+ uniprot_to_hugo,
38
+ )
39
+
40
+ logger = daiquiri.getLogger(__logger_name__ + ".build.seq_for_mut_prob")
41
+
42
+
43
+ #===========
44
+ # region Initialize
45
+ #===========
46
+
47
+ def initialize_seq_df(input_path, uniprot_to_gene_dict):
48
+ """
49
+ Parse any PDB structure from a given directory and create
50
+ a dataframe including HUGO symbol, Uniprot-ID, AF fragment,
51
+ and protein sequence.
52
+ """
53
+
54
+ list_prot_path = get_pdb_path_list_from_dir(input_path)
55
+ gene_lst = []
56
+ uni_id_lst = []
57
+ f_lst = []
58
+ seq_lst = []
59
+
60
+ for path_structure in tqdm(list_prot_path, total=len(list_prot_path), desc="Generating sequence df"):
61
+ uniprot_id, f = get_af_id_from_pdb(path_structure).split("-F")
62
+ if uniprot_id in uniprot_to_gene_dict:
63
+ gene = uniprot_to_gene_dict[uniprot_id]
64
+ else:
65
+ gene = np.nan
66
+ seq = "".join(list(get_seq_from_pdb(path_structure)))
67
+ gene_lst.append(gene)
68
+ uni_id_lst.append(uniprot_id)
69
+ f_lst.append(f)
70
+ seq_lst.append(seq)
71
+
72
+ seq_df = pd.DataFrame({"Gene" : gene_lst,
73
+ "Uniprot_ID" : uni_id_lst,
74
+ "F" : f_lst,
75
+ "Seq" : seq_lst
76
+ }).sort_values(["Gene", "F"])
77
+
78
+ return seq_df
79
+
80
+
81
+ #==============================================
82
+ # Get DNA sequence using EMBL backtranseq API
83
+ # (not 100% reliable but available fo most seq)
84
+ #==============================================
85
+
86
+ def backtranseq(protein_seqs, organism = "Homo sapiens"):
87
+ """
88
+ Perform backtranslation from proteins to DNA sequences using EMBOS backtranseq.
89
+ """
90
+
91
+ # Define the API endpoints
92
+ run_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/run"
93
+ status_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/status/"
94
+ result_url = "https://www.ebi.ac.uk/Tools/services/rest/emboss_backtranseq/result/"
95
+
96
+ # Define the parameters for the API request (an email address must be included)
97
+ params = {"email": "stefano.pellegrini@irbbarcelona.org",
98
+ "sequence": protein_seqs,
99
+ "outseqformat": "plain",
100
+ "molecule": "dna",
101
+ "organism": organism}
102
+
103
+ # Submit the job request and retrieve the job ID
104
+ response = "INIT"
105
+ while str(response) != "<Response [200]>":
106
+ if response != "INIT":
107
+ time.sleep(10)
108
+ try:
109
+ response = requests.post(run_url, data=params, timeout=160)
110
+ except requests.exceptions.RequestException as e:
111
+ response = "ERROR"
112
+ logger.debug(f"Request failed: {e}")
113
+
114
+ job_id = response.text.strip()
115
+
116
+ # Wait for the job to complete
117
+ status = "INIT"
118
+ while status != "FINISHED":
119
+ time.sleep(20)
120
+ try:
121
+ result = requests.get(status_url + job_id, timeout=160)
122
+ status = result.text.strip()
123
+ except requests.exceptions.RequestException as e:
124
+ status = "ERROR"
125
+ logger.debug(f"Request failed {e}: Retrying..")
126
+
127
+ # Retrieve the results of the job
128
+ status = "INIT"
129
+ while status != "FINISHED":
130
+ try:
131
+ result = requests.get(result_url + job_id + "/out", timeout=160)
132
+ status = "FINISHED"
133
+ except requests.exceptions.RequestException as e:
134
+ status = "ERROR"
135
+ logger.debug(f"Request failed {e}: Retrying..")
136
+ time.sleep(10)
137
+
138
+ dna_seq = result.text.strip()
139
+
140
+ return dna_seq
141
+
142
+
143
+ def batch_backtranseq(df, batch_size, organism = "Homo sapiens"):
144
+ """
145
+ Given a dataframe including protein sequences, it divides the
146
+ sequences into batches of a given size and run EMBOSS backtranseq
147
+ (https://www.ebi.ac.uk/Tools/st/emboss_backtranseq/) to translate
148
+ them into DNA sequences.
149
+ """
150
+
151
+ batches = df.groupby(df.index // batch_size)
152
+ lst_batches = []
153
+
154
+ # Iterate over batches
155
+ for i, batch in tqdm(batches, total=len(batches), desc="Backtranseq"):
156
+
157
+ # Avoid sending too many request
158
+ if i+1 == 30:
159
+ logger.debug("Reached maximum number of requests: waiting 180s..")
160
+ time.sleep(180)
161
+
162
+ # Get input format for backtranseq
163
+ batch_seq = "\n".join(batch.reset_index(drop=True).apply(lambda x: f'>\n{x["Seq"]}', axis=1).values)
164
+
165
+ # Run backtranseq
166
+ batch_dna = backtranseq(batch_seq, organism = organism)
167
+
168
+ # Parse output
169
+ batch_dna = re.split(">EMBOSS_\d+", batch_dna.replace("\n", ""))[1:]
170
+
171
+ batch["Seq_dna"] = batch_dna
172
+ lst_batches.append(batch)
173
+
174
+ return pd.concat(lst_batches)
175
+
176
+
177
+ #===============================================================
178
+ # Get exons (CDS) coordinate using EMBL Proteins Coordinates API
179
+ #===============================================================
180
+
181
+ def _uniprot_request_coord(lst_uniprot_ids):
182
+ """
183
+ Use Coordinates from EMBL-EBI Proteins API to get
184
+ a json including exons coordinate and protein info.
185
+
186
+ https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
187
+ https://doi.org/10.1093/nar/gkx237
188
+ """
189
+
190
+ prot_request = [f"{prot}" if i == 0 else f"%2C{prot}" for i, prot in enumerate(lst_uniprot_ids)]
191
+ requestURL = f"https://www.ebi.ac.uk/proteins/api/coordinates?offset=0&size=100&accession={''.join(prot_request)}"
192
+
193
+ status = "INIT"
194
+ while status != "FINISHED":
195
+ if status != "INIT":
196
+ time.sleep(10)
197
+ try:
198
+ r = requests.get(requestURL, headers={ "Accept" : "application/json"}, timeout=160)
199
+ if r.ok:
200
+ status = "FINISHED"
201
+ else:
202
+ logger.debug(f"Error occurred after successfully sending request {r.raise_for_status()}: Retrying..")
203
+ status = "ERROR"
204
+ except requests.exceptions.RequestException as e:
205
+ status = "ERROR"
206
+ logger.debug(f"Request failed ({e}): Retrying..")
207
+
208
+ for dictio in json.loads(r.text):
209
+
210
+ yield dictio
211
+
212
+
213
+ def get_sorted_transcript_lst(dictio, canonical_ids):
214
+ """
215
+ Sort a list of tuple (index, transcript ID) placing the
216
+ Ensembl canonical transcript as first element, if present.
217
+ """
218
+
219
+ lst_tuple_ix_id = [(i, coord_dict["ensemblTranscriptId"]) for (i, coord_dict) in enumerate(dictio)]
220
+
221
+ return sorted(lst_tuple_ix_id, key=lambda x: x[1] in canonical_ids, reverse=True)
222
+
223
+
224
+ def get_batch_exons_coord(batch_ids, ens_canonical_transcripts_lst):
225
+ """
226
+ Parse the json obtained from the Coordinates service extracting
227
+ exons coordinates and protein info.
228
+
229
+ https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
230
+ https://doi.org/10.1093/nar/gkx237
231
+ """
232
+
233
+ lst_uni_id = []
234
+ lst_ens_gene_id = []
235
+ lst_ens_transcr_id = []
236
+ lst_seq = []
237
+ lst_chr = []
238
+ lst_reverse = []
239
+ lst_ranges = []
240
+
241
+ for dic in _uniprot_request_coord(batch_ids):
242
+
243
+ uni_id = dic["accession"]
244
+ seq = dic["sequence"]
245
+
246
+ # Iterate throug the transcripts (starting from Ensembl canonical one if present)
247
+ sorted_transcript_lst = get_sorted_transcript_lst(dic["gnCoordinate"], ens_canonical_transcripts_lst)
248
+ for i, ens_transcr_id in sorted_transcript_lst:
249
+
250
+ ens_gene_id = dic["gnCoordinate"][i]["ensemblGeneId"]
251
+ dic_loc = dic["gnCoordinate"][i]["genomicLocation"]
252
+ exons = dic_loc["exon"]
253
+ chromosome = dic_loc["chromosome"]
254
+ reverse_strand = dic_loc["reverseStrand"]
255
+ ranges = []
256
+
257
+ for exon in exons:
258
+ exon = exon["genomeLocation"]
259
+ if "begin" in exon.keys():
260
+ begin = exon["begin"]["position"]
261
+ end = exon["end"]["position"]
262
+ else:
263
+ begin = exon["position"]["position"]
264
+ end = exon["position"]["position"]
265
+ ranges.append((begin, end))
266
+
267
+ # Check if the DNA seq of the transcript is equal the codon lenght
268
+ start = 0 if not int(reverse_strand) else 1
269
+ end = 1 if not int(reverse_strand) else 0
270
+ len_dna = sum([coord[end] - coord[start] + 1 for coord in ranges])
271
+ if len_dna / 3 == len(seq):
272
+ break
273
+ # If there is no transcript with matching sequence length, return NA
274
+ elif i == len(dic["gnCoordinate"]) - 1:
275
+ ens_gene_id = np.nan
276
+ ens_transcr_id = np.nan
277
+ chromosome = np.nan
278
+ reverse_strand = np.nan
279
+ ranges = np.nan
280
+
281
+ lst_uni_id.append(uni_id)
282
+ lst_ens_gene_id.append(ens_gene_id)
283
+ lst_ens_transcr_id.append(ens_transcr_id)
284
+ lst_seq.append(seq)
285
+ lst_chr.append(chromosome)
286
+ reverse_strand = int(reverse_strand) if isinstance(reverse_strand, int) else np.nan
287
+ ranges = str(ranges) if isinstance(ranges, list) else np.nan
288
+ lst_reverse.append(reverse_strand)
289
+ lst_ranges.append(ranges)
290
+
291
+ return pd.DataFrame({"Uniprot_ID" : lst_uni_id,
292
+ "Ens_Gene_ID" : lst_ens_gene_id,
293
+ "Ens_Transcr_ID" : lst_ens_transcr_id,
294
+ "Seq" : lst_seq,
295
+ "Chr" : lst_chr,
296
+ "Reverse_strand" : lst_reverse,
297
+ "Exons_coord" : lst_ranges})
298
+
299
+
300
+ def get_exons_coord(ids, ens_canonical_transcripts_lst, batch_size=100):
301
+ """
302
+ Use the Coordinates service from Proteins API of EMBL-EBI to get
303
+ exons coordinate and proteins info of all provided Uniprot IDs.
304
+
305
+ https://www.ebi.ac.uk/proteins/api/doc/#coordinatesApi
306
+ https://doi.org/10.1093/nar/gkx237
307
+ """
308
+
309
+ lst_df = []
310
+ batches_ids = [ids[i:i+batch_size] for i in range(0, len(ids), batch_size)]
311
+
312
+ for batch_ids in tqdm(batches_ids, total=len(batches_ids), desc="Adding exons coordinate"):
313
+
314
+ batch_df = get_batch_exons_coord(batch_ids, ens_canonical_transcripts_lst)
315
+
316
+ # Identify unmapped IDs and add them as NaN rows
317
+ unmapped_ids = list(set(batch_ids).difference(set(batch_df.Uniprot_ID.unique())))
318
+ nan = np.repeat(np.nan, len(unmapped_ids))
319
+ nan_rows = pd.DataFrame({'Uniprot_ID' : unmapped_ids,
320
+ 'Ens_Gene_ID' : nan,
321
+ 'Ens_Transcr_ID' : nan,
322
+ 'Seq': nan,
323
+ 'Chr': nan,
324
+ 'Reverse_strand' : nan,
325
+ 'Exons_coord': nan})
326
+
327
+ batch_df = pd.concat([batch_df, nan_rows], ignore_index=True)
328
+ lst_df.append(batch_df)
329
+
330
+ return pd.concat(lst_df).reset_index(drop=True)
331
+
332
+
333
+ #=======================================================================
334
+ # Get DNA sequence and trin-context of reference genome from coordinates
335
+ #=======================================================================
336
+
337
+ def per_site_trinucleotide_context(seq, no_flanks=False):
338
+ """
339
+ Given a DNA sequence rapresenting an exon (CDS) with -1 and +1 flanking
340
+ sites, return a list including the trinucletide context of each site.
341
+ """
342
+
343
+ # If there is no info about flanking regions, add most the two most common ones
344
+ if no_flanks:
345
+ seq = f"C{seq}T"
346
+
347
+ return [f"{seq[i-1]}{seq[i]}{seq[i+1]}" for i in range(1, len(seq) - 1)]
348
+
349
+
350
+ def get_ref_dna_and_context(row, genome_fun):
351
+ """
352
+ Given a row of the sequence dataframe including exons (CDS) coordinate,
353
+ get the corresponding DNA sequence and the trinucleotide context of each
354
+ site of the sequence taking into account flaking regions at splicing sites.
355
+ """
356
+
357
+ transcript_id = row["Ens_Transcr_ID"]
358
+
359
+ try:
360
+ lst_exon_coord = ast.literal_eval(row["Exons_coord"])
361
+ reverse = row["Reverse_strand"]
362
+ chrom = row["Chr"]
363
+
364
+ lst_exon_tri_context = []
365
+ lst_exon_seq = []
366
+
367
+ for region in lst_exon_coord:
368
+
369
+ # Retrieve reference seq of the exon (CDS)
370
+ start = 0 if not reverse else 1
371
+ end = 1 if not reverse else 0
372
+ segment_len = region[end] - region[start] + 1
373
+ seq_with_flanks = genome_fun(chrom, region[start] - 1, size = segment_len + 2)
374
+
375
+ # Get reverse complement if reverse strand
376
+ if reverse:
377
+ seq_with_flanks = str(Seq(seq_with_flanks).reverse_complement())
378
+
379
+ # Get the trinucleotide context of each site of the exon (CDS)
380
+ per_site_tri_context = ",".join(per_site_trinucleotide_context(seq_with_flanks))
381
+
382
+ # Get the reference DNA sequence
383
+ seq = seq_with_flanks[1:-1]
384
+
385
+ lst_exon_tri_context.append(per_site_tri_context)
386
+ lst_exon_seq.append(seq)
387
+
388
+ return transcript_id, "".join(lst_exon_seq), ",".join(lst_exon_tri_context), 1
389
+
390
+ except Exception as e:
391
+ if not str(e).startswith("Sequence"):
392
+ logger.warning(f"Error occurred during retrieving DNA seq from ref coordinates {transcript_id} {e}: Skipping..")
393
+
394
+ return np.nan, np.nan, np.nan, -1
395
+
396
+
397
+ def add_ref_dna_and_context(seq_df, genome_fun=hg38):
398
+ """
399
+ Given as input the sequence dataframe including exons (CDS) coordinate,
400
+ add the corresponding DNA sequence and the trinucleotide context of each
401
+ site of the sequence taking into account flaking regions at splicing sites.
402
+ """
403
+
404
+ seq_df = seq_df.copy()
405
+ seq_df_with_coord = seq_df[["Ens_Transcr_ID", "Chr", "Reverse_strand", "Exons_coord"]].dropna().drop_duplicates()
406
+ ref_dna_and_context = seq_df_with_coord.apply(lambda x: get_ref_dna_and_context(x, genome_fun), axis=1, result_type='expand')
407
+ ref_dna_and_context.columns = ["Ens_Transcr_ID", "Seq_dna", "Tri_context", "Reference_info"]
408
+ seq_df = seq_df.merge(ref_dna_and_context, on=["Ens_Transcr_ID"], how="left").drop_duplicates().reset_index(drop=True)
409
+ seq_df["Reference_info"] = seq_df["Reference_info"].fillna(-1).astype(int)
410
+ seq_df.loc[seq_df["Reference_info"] == -1, "Ens_Transcr_ID"] = np.nan
411
+ seq_df.loc[seq_df["Reference_info"] == -1, "Seq_dna"] = np.nan
412
+ seq_df.loc[seq_df["Reference_info"] == -1, "Exons_coord"] = np.nan
413
+ seq_df.loc[seq_df["Reference_info"] == -1, "Tri_context"] = np.nan
414
+
415
+ return seq_df
416
+
417
+
418
+ #=========
419
+ # WRAPPERS
420
+ #=========
421
+
422
+ def add_extra_genes_to_seq_df(seq_df, uniprot_to_gene_dict):
423
+ """
424
+ If multiple genes are mapping to a given Uniprot_ID, add
425
+ each gene name with corresponding sequence info to the seq_df.
426
+ """
427
+
428
+ lst_added_genes = []
429
+ lst_extra_genes_rows = []
430
+ for _, seq_row in seq_df.iterrows():
431
+ uni_id = seq_row["Uniprot_ID"]
432
+ if uni_id in uniprot_to_gene_dict:
433
+ gene_id = uniprot_to_gene_dict[uni_id]
434
+
435
+ if not pd.isna(gene_id):
436
+ gene_id = gene_id.split()
437
+
438
+ if len(gene_id) > 1:
439
+ for gene in gene_id:
440
+ if gene != seq_row["Gene"] and gene not in lst_added_genes:
441
+ row = seq_row.copy()
442
+ row["Gene"] = gene
443
+ lst_extra_genes_rows.append(row)
444
+ lst_added_genes.append(gene)
445
+
446
+ seq_df_extra_genes = pd.concat(lst_extra_genes_rows, axis=1).T
447
+
448
+ # Remove rows with multiple symbols and drop duplicated ones
449
+ seq_df = pd.concat((seq_df, seq_df_extra_genes))
450
+ seq_df = seq_df.dropna(subset=["Gene"])
451
+ seq_df = seq_df[seq_df.apply(lambda x: len(x["Gene"].split()), axis =1) == 1].reset_index(drop=True)
452
+ seq_df = seq_df.drop_duplicates().reset_index(drop=True)
453
+
454
+ return seq_df
455
+
456
+
457
+ def download_mane_summary(path_to_file, v=1.3, max_attempts=15):
458
+ """
459
+ Download the summary.txt of the MANE release from NCBI.
460
+ """
461
+
462
+ mane_summary_url = f"https://ftp.ncbi.nlm.nih.gov/refseq/MANE/MANE_human/release_{v}/MANE.GRCh38.v{v}.summary.txt.gz"
463
+ attempts = 0
464
+
465
+ while not os.path.exists(path_to_file):
466
+ download_single_file(mane_summary_url, path_to_file, threads=1)
467
+ attempts += 1
468
+ if attempts >= max_attempts:
469
+ raise RuntimeError(f"Failed to download MANE summary file after {max_attempts} attempts. Exiting..")
470
+ time.sleep(5)
471
+
472
+
473
+ def select_uni_id(ids_tuple, all_ids):
474
+ """
475
+ Return the first Uniprot ID present in the list of IDs
476
+ (list of structures available). If no ID of the tuple
477
+ maps a downloaded structure, return NA.
478
+ """
479
+
480
+ for uni_id in ids_tuple:
481
+ if uni_id in all_ids:
482
+ return uni_id
483
+
484
+ return np.nan
485
+
486
+
487
+ def get_mane_to_af_mapping(datasets_dir, downloaded_uniprot_ids, include_not_af=False, mane_version=1.0):
488
+ """
489
+ Get a dataframe to map genes, MANE transcript IDs, and protein structures.
490
+ """
491
+
492
+ mane_to_af = pd.read_csv(os.path.join(datasets_dir, "mane_refseq_prot_to_alphafold.csv"))
493
+ mane_to_af = mane_to_af.rename(columns={"refseq_prot" : "Refseq_prot",
494
+ "uniprot_accession" : "Uniprot_ID"}).drop(columns=["alphafold"])
495
+ path_mane_summary = os.path.join(datasets_dir, "mane_summary.txt.gz")
496
+ if not os.path.exists(path_mane_summary):
497
+ download_mane_summary(path_mane_summary, mane_version)
498
+ mane = pd.read_csv(path_mane_summary, compression='gzip', sep="\t").dropna(subset=["symbol", "HGNC_ID"])
499
+ mane = mane.rename(columns={"symbol" : "Gene",
500
+ "RefSeq_prot" : "Refseq_prot",
501
+ "Ensembl_Gene" : "Ens_Gene_ID",
502
+ "Ensembl_nuc" : "Ens_Transcr_ID",
503
+ "GRCh38_chr" : "Chr",
504
+ "chr_strand" : "Reverse_strand"})
505
+ mane = mane[["Gene", "Refseq_prot", "Ens_Gene_ID", "Ens_Transcr_ID", "Chr", "Reverse_strand"]]
506
+ mane_mapping = mane_to_af.merge(mane, how="left", on="Refseq_prot").dropna()
507
+ mane_mapping.Reverse_strand = mane_mapping.Reverse_strand.map({"+" : 0, "-" : 1})
508
+ mane_mapping.Ens_Gene_ID = mane_mapping.Ens_Gene_ID.apply(lambda x: x.split(".")[0])
509
+ mane_mapping.Ens_Transcr_ID = mane_mapping.Ens_Transcr_ID.apply(lambda x: x.split(".")[0])
510
+
511
+ # Select first Uniprot ID if multiple ones are present
512
+ mane_mapping["Uniprot_ID"] = mane_mapping.apply(lambda x:
513
+ select_uni_id(x.Uniprot_ID.split(";"), downloaded_uniprot_ids) if len(x.Uniprot_ID.split(";")) > 1
514
+ else x.Uniprot_ID, axis=1)
515
+ mane_mapping = mane_mapping.reset_index(drop=True)
516
+
517
+ if include_not_af:
518
+ mane_not_af = mane[~mane.Gene.isin(mane_mapping.Gene)].reset_index(drop=True)
519
+
520
+ return mane_mapping, mane_not_af
521
+
522
+ else:
523
+ return mane_mapping
524
+
525
+
526
+ def download_biomart_metadata(path_to_file):
527
+ """
528
+ Query biomart to get the list of transcript corresponding to the downloaded
529
+ structures (a few structures are missing) and other information.
530
+ """
531
+
532
+ # command = f"""
533
+ # wget -O {path_to_file} 'http://jan2024.archive.ensembl.org/biomart/martservice?query=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Query><Query virtualSchemaName="default" formatter="TSV" header="0" uniqueRows="0" count="" datasetConfigVersion="0.6"><Dataset name="hsapiens_gene_ensembl" interface="default"><Attribute name="ensembl_gene_id"/><Attribute name="ensembl_transcript_id"/><Attribute name="transcript_is_canonical"/><Attribute name="external_gene_name"/><Attribute name="external_gene_source"/><Attribute name="hgnc_id"/><Attribute name="uniprot_gn_id"/><Attribute name="uniprotswissprot"/></Dataset></Query>'
534
+ # """
535
+
536
+ command = f"""
537
+ wget -O {path_to_file} 'http://jan2024.archive.ensembl.org/biomart/martservice?query=<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Query><Query virtualSchemaName="default" formatter="TSV" header="0" uniqueRows="0" count="" datasetConfigVersion="0.6"><Dataset name="hsapiens_gene_ensembl" interface="default"><Attribute name="ensembl_gene_id"/><Attribute name="ensembl_transcript_id"/><Attribute name="transcript_is_canonical"/><Attribute name="external_gene_name"/><Attribute name="external_gene_source"/><Attribute name="hgnc_id"/><Attribute name="uniprot_gn_id"/><Attribute name="uniprotswissprot"/><Attribute name="external_synonym"/></Dataset></Query>'
538
+ """
539
+
540
+ subprocess.run(shlex.split(command))
541
+
542
+
543
+ def get_biomart_metadata(datasets_dir, uniprot_ids):
544
+ """
545
+ Download a dataframe including ensembl canonical transcript IDs,
546
+ HGNC IDs, Uniprot IDs, and other useful information.
547
+ """
548
+
549
+ download_biomart_metadata(os.path.join(datasets_dir, "biomart_metadata.tsv"))
550
+
551
+ # Parse
552
+ biomart_df = pd.read_csv(os.path.join(datasets_dir, "biomart_metadata.tsv"), sep="\t", header=None)
553
+ biomart_df.columns= ["Ens_Gene_ID", "Ens_Transcr_ID", "Ens_Canonical", "Gene", "Gene_source", "HGNC_ID", "Uniprot_ID", "UniprotKB_ID", "Gene_synonym"]
554
+ biomart_df = biomart_df.dropna(subset=["Uniprot_ID", "UniprotKB_ID"], how='all')
555
+ biomart_df = biomart_df[biomart_df["Gene_source"] == "HGNC Symbol"].drop(columns=["Gene_source"])
556
+ biomart_df.reset_index(inplace=True, drop=True)
557
+
558
+ # Filter
559
+ uniprot_ids = pd.Series(uniprot_ids)
560
+ uniprot_kb_ids = uniprot_ids[(uniprot_ids.isin(biomart_df.UniprotKB_ID))]
561
+ uniprot_notkb_ids = uniprot_ids[~(uniprot_ids.isin(biomart_df.UniprotKB_ID)) & (uniprot_ids.isin(biomart_df.Uniprot_ID))]
562
+ biomart_uniprotkb = biomart_df[biomart_df.UniprotKB_ID.isin(uniprot_kb_ids)].drop(
563
+ columns=["Uniprot_ID"]).rename(columns={"UniprotKB_ID" : "Uniprot_ID"}).drop_duplicates()
564
+ biomart_uniprot = biomart_df[biomart_df.Uniprot_ID.isin(uniprot_notkb_ids)].drop(columns=["UniprotKB_ID"]).drop_duplicates()
565
+ biomart_df = pd.concat((biomart_uniprotkb, biomart_uniprot)).reset_index(drop=True)
566
+
567
+ # Output
568
+ # (ATM this is only used to get Ensembl canonical transcript
569
+ # but, in the future, it could be used to add extra info such as
570
+ # HGNC, and any other features we want from biomart)
571
+ biomart_df.to_csv(os.path.join(datasets_dir, "biomart_metadata.tsv"), sep="\t", index=False)
572
+ ens_canonical_transcripts = biomart_df.Ens_Transcr_ID[biomart_df["Ens_Canonical"] == 1].unique()
573
+ # gene_ids = biomart_df[["Gene", "HGNC_ID"]]
574
+ # gene_ids_syn = biomart_df[["Gene_synonym", "HGNC_ID"]].rename(columns={"Gene_synonym" : "Gene"})
575
+ # gene_ids = pd.concat((gene_ids, gene_ids_syn)).sort_values("HGNC_ID").reset_index(drop=True).drop_duplicates()
576
+
577
+ return ens_canonical_transcripts
578
+
579
+
580
+ def get_ref_dna_from_ensembl(transcript_id):
581
+ """
582
+ Use Ensembl GET sequence rest API to obtain CDS DNA
583
+ sequence from Ensembl transcript ID.
584
+
585
+ https://rest.ensembl.org/documentation/info/sequence_id
586
+ """
587
+
588
+ server = "https://rest.ensembl.org"
589
+ ext = f"/sequence/id/{transcript_id}?type=cds"
590
+
591
+ status = "INIT"
592
+ i = 0
593
+ while status != "FINISHED":
594
+
595
+ try:
596
+ r = requests.get(server+ext, headers={ "Content-Type" : "text/x-fasta"}, timeout=160)
597
+ if not r.ok:
598
+ r.raise_for_status()
599
+
600
+ status = "ERROR"
601
+ sys.exit()
602
+ else:
603
+ status = "FINISHED"
604
+
605
+ except requests.exceptions.RequestException as e:
606
+ i += 1
607
+ status = "ERROR"
608
+ if i%10 == 0:
609
+ logger.debug(f"Failed to retrieve sequence for {transcript_id} {e}: Retrying..")
610
+ time.sleep(5)
611
+ if i == 100:
612
+ logger.debug(f"Failed to retrieve sequence for {transcript_id} {e}: Skipping..")
613
+ return np.nan
614
+
615
+ time.sleep(1)
616
+
617
+ seq_dna = "".join(r.text.strip().split("\n")[1:])
618
+
619
+ return seq_dna[:len(seq_dna)-3]
620
+
621
+
622
+ def get_ref_dna_from_ensembl_wrapper(ensembl_id):
623
+ """
624
+ Wrapper for multiple processing function using
625
+ Ensembl Get sequence rest API.
626
+ """
627
+
628
+ return get_ref_dna_from_ensembl(ensembl_id)
629
+
630
+
631
+ def get_ref_dna_from_ensembl_mp(seq_df, cores):
632
+ """
633
+ Multiple processing function to use Ensembl GET sequence
634
+ rest API to obtain CDS DNA sequence from Ensembl transcript ID.
635
+
636
+ https://rest.ensembl.org/documentation/info/sequence_id
637
+ """
638
+
639
+ pool = multiprocessing.Pool(processes=cores)
640
+ seq_df = seq_df.copy()
641
+ seq_df["Seq_dna"] = pool.map(get_ref_dna_from_ensembl_wrapper, seq_df.Ens_Transcr_ID)
642
+ pool.close()
643
+ pool.join()
644
+
645
+ return seq_df
646
+
647
+
648
+ def drop_gene_duplicates(df):
649
+ """
650
+ Issue: multiple Uniprot IDs might map to the same HUGO symbol.
651
+ Drop Uniprot ID of gene duplicates by prioritizing reference
652
+ info status (Proteins API > Ensembl API > Backtranseq API).
653
+ """
654
+
655
+ df = df.copy()
656
+ df.Uniprot_ID = df.Uniprot_ID.str.replace(";", "")
657
+ df.Gene = df.Gene.str.replace(";", "")
658
+ df = df.sort_values(["Gene", "Reference_info"], ascending=False).drop_duplicates(subset='Gene').reset_index(drop=True)
659
+
660
+ # Check if there are still duplicates
661
+ n_duplicates = sum(df["Gene"].value_counts() > 1)
662
+ if n_duplicates > 0:
663
+ logger.warning(f"Found {n_duplicates} duplicates gene entries: Mapping HUGO Symbol to protein info might be affected.")
664
+ else:
665
+ logger.debug("Duplicates gene entries correctly removed!")
666
+
667
+ return df
668
+
669
+
670
+ def process_seq_df(seq_df,
671
+ datasets_dir,
672
+ organism,
673
+ uniprot_to_gene_dict,
674
+ ens_canonical_transcripts_lst,
675
+ num_cores=1,
676
+ rm_weird_chr=False,
677
+ mane_version=1.3):
678
+ """
679
+ Retrieve DNA sequence and tri-nucleotide context
680
+ for each structure in the initialized dataframe
681
+ prioritizing structures obtained from transcripts
682
+ whose exon coordinates are available in the Proteins API.
683
+
684
+ Reference_info labels:
685
+ 1 : Transcript ID, exons coord, seq DNA obtained from Proteins API
686
+ -1 : Not available transcripts, seq DNA retrieved from Backtranseq API
687
+ """
688
+
689
+ # Process entries in Proteins API (Reference_info 1)
690
+ #---------------------------------------------------
691
+
692
+ # Add coordinates for mutability integration (entries in Proteins API)
693
+ logger.debug(f"Retrieving CDS DNA seq from reference genome (Proteins API): {len(seq_df['Uniprot_ID'].unique())} structures..")
694
+ coord_df = get_exons_coord(seq_df["Uniprot_ID"].unique(), ens_canonical_transcripts_lst)
695
+ seq_df = seq_df.merge(coord_df, on=["Seq", "Uniprot_ID"], how="left").reset_index(drop=True)
696
+
697
+ # Add ref DNA seq and its per-site trinucleotide context (entries in Proteins API)
698
+ if organism == "Homo sapiens":
699
+ logger.debug("Loading reference genome hg38..")
700
+ genome_fun = hg38
701
+ elif organism == "Mus musculus":
702
+ logger.debug("Loading reference genome mm39..")
703
+ genome_fun = mm39
704
+ else:
705
+ raise RuntimeError(f"Failed to recognize '{organism}' as organism. Currently accepted ones are 'Homo sapiens' and 'Mus musculus'. Exiting..")
706
+ seq_df = add_ref_dna_and_context(seq_df, genome_fun)
707
+ seq_df_uniprot = seq_df[seq_df["Reference_info"] == 1]
708
+ seq_df_not_uniprot = seq_df[seq_df["Reference_info"] == -1]
709
+
710
+
711
+ # Process entries not in Proteins API (Reference_info -1)
712
+ #------------------------------------------------------------
713
+
714
+ # Add DNA seq from Backtranseq for any other entry
715
+ logger.debug(f"Retrieving CDS DNA seq for entries without available transcript ID (Backtranseq API): {len(seq_df_not_uniprot)} structures..")
716
+ seq_df_not_uniprot = batch_backtranseq(seq_df_not_uniprot, 500, organism=organism)
717
+
718
+ # Get trinucleotide context
719
+ seq_df_not_uniprot["Tri_context"] = seq_df_not_uniprot["Seq_dna"].apply(
720
+ lambda x: ",".join(per_site_trinucleotide_context(x, no_flanks=True)))
721
+
722
+
723
+ # Prepare final output
724
+ #---------------------
725
+
726
+ # Concat the dfs, expand multiple genes associated to the same structure, keep only one structure for each gene
727
+ seq_df = pd.concat((seq_df_uniprot, seq_df_not_uniprot)).reset_index(drop=True)
728
+ logger_report = ", ".join([f"{v}: {c}" for (v, c) in zip(seq_df.Reference_info.value_counts().index,
729
+ seq_df.Reference_info.value_counts().values)])
730
+ logger.info(f"Built of sequence dataframe completed. Retrieved {len(seq_df)} structures ({logger_report})")
731
+ seq_df = add_extra_genes_to_seq_df(seq_df, uniprot_to_gene_dict)
732
+ seq_df = drop_gene_duplicates(seq_df)
733
+
734
+ return seq_df
735
+
736
+
737
+ def process_seq_df_mane(seq_df,
738
+ datasets_dir,
739
+ uniprot_to_gene_dict,
740
+ ens_canonical_transcripts_lst,
741
+ num_cores=1,
742
+ mane_version=1.3):
743
+ """
744
+ Retrieve DNA sequence and tri-nucleotide context
745
+ for each structure in the initialized dataframe
746
+ prioritizing MANE associated structures and metadata.
747
+
748
+ Reference_info labels:
749
+ 1 : Transcript ID, exons coord, seq DNA obtained from Proteins API
750
+ 0 : Transcript ID retrieved from MANE and seq DNA from Ensembl
751
+ -1 : Not available transcripts, seq DNA retrieved from Backtranseq API
752
+ """
753
+
754
+ mane_mapping, mane_mapping_not_af = get_mane_to_af_mapping(datasets_dir,
755
+ seq_df["Uniprot_ID"].unique(),
756
+ include_not_af=True,
757
+ mane_version=mane_version)
758
+ seq_df_mane = seq_df[seq_df.Uniprot_ID.isin(mane_mapping.Uniprot_ID)].reset_index(drop=True)
759
+ seq_df_nomane = seq_df[~seq_df.Uniprot_ID.isin(mane_mapping.Uniprot_ID)].reset_index(drop=True)
760
+
761
+ # Seq df MANE
762
+ seq_df_mane = seq_df_mane.drop(columns=["Gene"]).merge(mane_mapping, how="left", on="Uniprot_ID")
763
+ seq_df_mane["Reference_info"] = 0
764
+
765
+ # Add DNA seq from Ensembl for structures with available transcript ID
766
+ logger.debug(f"Retrieving CDS DNA seq from transcript ID (Ensembl API): {len(seq_df_mane)} structures..")
767
+ seq_df_mane = get_ref_dna_from_ensembl_mp(seq_df_mane, cores=num_cores)
768
+
769
+ # Set failed and len-mismatching entries as no-transcripts entries
770
+ failed_ix = seq_df_mane.apply(lambda x: True if pd.isna(x.Seq_dna) else len(x.Seq_dna) / 3 != len(x.Seq), axis=1)
771
+ if sum(failed_ix) > 0:
772
+ seq_df_mane_failed = seq_df_mane[failed_ix]
773
+ seq_df_mane = seq_df_mane[~failed_ix]
774
+ seq_df_mane_failed = seq_df_mane_failed.drop(columns=["Ens_Gene_ID", "Ens_Transcr_ID", "Reverse_strand",
775
+ "Chr", "Refseq_prot", "Reference_info", "Seq_dna"])
776
+ seq_df_nomane = pd.concat((seq_df_nomane, seq_df_mane_failed))
777
+
778
+
779
+ # Seq df not MANE
780
+ seq_df_nomane = add_extra_genes_to_seq_df(seq_df_nomane, uniprot_to_gene_dict) # Filter out genes with NA
781
+ seq_df_nomane = seq_df_nomane[seq_df_nomane.Gene.isin(mane_mapping_not_af.Gene)] # Filter out genes that are not in MANE list
782
+
783
+ # Retrieve seq from coordinates
784
+ logger.debug(f"Retrieving CDS DNA seq from reference genome (Proteins API): {len(seq_df_nomane['Uniprot_ID'].unique())} structures..")
785
+ coord_df = get_exons_coord(seq_df_nomane["Uniprot_ID"].unique(), ens_canonical_transcripts_lst)
786
+ seq_df_nomane = seq_df_nomane.merge(coord_df, on=["Seq", "Uniprot_ID"], how="left").reset_index(drop=True) # Discard entries whose Seq obtained by Proteins API don't exactly match the one in structure
787
+ seq_df_nomane = add_ref_dna_and_context(seq_df_nomane, hg38)
788
+ seq_df_nomane_tr = seq_df_nomane[seq_df_nomane["Reference_info"] == 1]
789
+ seq_df_nomane_notr = seq_df_nomane[seq_df_nomane["Reference_info"] == -1]
790
+
791
+ # Add DNA seq from Backtranseq for any other entry
792
+ logger.debug(f"Retrieving CDS DNA seq for genes without available transcript ID (Backtranseq API): {len(seq_df_nomane_notr)} structures..")
793
+ seq_df_nomane_notr = batch_backtranseq(seq_df_nomane_notr, 500, organism="Homo sapiens")
794
+
795
+ # Get trinucleotide context
796
+ seq_df_not_uniprot = pd.concat((seq_df_mane, seq_df_nomane_notr))
797
+ seq_df_not_uniprot["Tri_context"] = seq_df_not_uniprot["Seq_dna"].apply(
798
+ lambda x: ",".join(per_site_trinucleotide_context(x, no_flanks=True)))
799
+
800
+ # Prepare final output
801
+ seq_df = pd.concat((seq_df_not_uniprot, seq_df_nomane_tr)).reset_index(drop=True)
802
+ seq_df = drop_gene_duplicates(seq_df)
803
+ report_df = seq_df.Reference_info.value_counts().reset_index()
804
+ report_df = report_df.rename(columns={"index" : "Source"})
805
+ report_df.Source = report_df.Source.map({1 : "Proteins API",
806
+ 0 : "MANE + Ensembl API",
807
+ -1 : "Backtranseq API"})
808
+ logger_report = ", ".join([f"{v}: {c}" for (v, c) in zip(report_df.Source,
809
+ report_df.Reference_info)])
810
+ logger.debug(f"Built of sequence dataframe completed. Retrieved {len(seq_df)} structures ({logger_report})")
811
+
812
+ return seq_df
813
+
814
+
815
+ def get_seq_df(datasets_dir,
816
+ output_seq_df,
817
+ organism = "Homo sapiens",
818
+ mane=False,
819
+ num_cores=1,
820
+ rm_weird_chr=False,
821
+ mane_version=1.3):
822
+ """
823
+ Generate a dataframe including IDs mapping information, the protein
824
+ sequence, the DNA sequence and its tri-nucleotide context, which is
825
+ used to compute the per-residue probability of missense mutation.
826
+
827
+ The DNA sequence and the tri-nucleotide context can be obtained by the
828
+ Proteins API (obtaining the coordinates of the exons and then using them
829
+ to obtain the sequence from the reference genome), Ensembl GET sequence API
830
+ (from Ensembl transcript ID), and from Backtranseq API for all other entries.
831
+
832
+ https://www.ebi.ac.uk/proteins/api/doc/
833
+ https://rest.ensembl.org/documentation/info/sequence_id
834
+ https://www.ebi.ac.uk/jdispatcher/st/emboss_backtranseq
835
+ """
836
+
837
+ # region Initialization
838
+ #===============
839
+
840
+ # Load Uniprot ID to HUGO and MANE to AF mapping
841
+ pdb_dir = os.path.join(datasets_dir, "pdb_structures")
842
+ uniprot_ids = os.listdir(pdb_dir)
843
+ uniprot_ids = [uni_id.split("-")[1] for uni_id in list(set(uniprot_ids)) if ".pdb" in uni_id]
844
+ logger.debug("Retrieving Uniprot ID to HUGO symbol mapping information..")
845
+ uniprot_to_gene_dict = uniprot_to_hugo(uniprot_ids)
846
+ # # Workaround if the direct request to UniprotKB stops working (it has happened temporarily)
847
+ # if all(pd.isna(k) for k in uniprot_to_gene_dict.keys()):
848
+ # logger.warning(f"Failed to retrieve Uniprot ID to HUGO symbol mapping directly from UniprotKB.")
849
+ # logger.warning(f"Retrying using Unipressed API client (only first HUGO symbol entry will be mapped)..")
850
+ # uniprot_to_gene_dict = uniprot_to_hugo_pressed(uniprot_ids)
851
+
852
+ # Get biomart metadata and canonical transcript IDs
853
+ ens_canonical_transcripts_lst = get_biomart_metadata(datasets_dir, uniprot_ids)
854
+
855
+ # Create a dataframe with protein sequences
856
+ logger.debug("Initializing sequence df..")
857
+ seq_df = initialize_seq_df(pdb_dir, uniprot_to_gene_dict)
858
+
859
+ if mane:
860
+ seq_df = process_seq_df_mane(seq_df,
861
+ datasets_dir,
862
+ uniprot_to_gene_dict,
863
+ ens_canonical_transcripts_lst,
864
+ num_cores,
865
+ mane_version=mane_version)
866
+ else:
867
+ seq_df = process_seq_df(seq_df,
868
+ datasets_dir,
869
+ organism,
870
+ uniprot_to_gene_dict,
871
+ ens_canonical_transcripts_lst,
872
+ num_cores,
873
+ rm_weird_chr,
874
+ mane_version=mane_version)
875
+
876
+
877
+ # # Filter out non-standard chromosomes
878
+ # chr_lst = [str(i) for i in range(1, 23)] + ['X', 'Y', 'M']
879
+ # seq_df = seq_df[seq_df.Chr.isin(chr_lst) | seq_df.Chr.isna()].reset_index(drop=True)
880
+
881
+ # Save
882
+ seq_df_cols = ['Gene', 'HGNC_ID', 'Ens_Gene_ID',
883
+ 'Ens_Transcr_ID', 'Uniprot_ID', 'F',
884
+ 'Seq', 'Chr', 'Reverse_strand', 'Exons_coord',
885
+ 'Seq_dna', 'Tri_context','Reference_info']
886
+ seq_df = seq_df[[col for col in seq_df_cols if col in seq_df.columns]]
887
+ seq_df.to_csv(output_seq_df, index=False, sep="\t")
888
+ logger.debug(f"Sequences dataframe saved in: {output_seq_df}")
889
+
890
+ return seq_df
891
+
892
+
893
+ if __name__ == "__main__":
894
+ OUTPUT_DS = 'test/output_datasets'
895
+ get_seq_df(datasets_dir=OUTPUT_DS,
896
+ output_seq_df=os.path.join(OUTPUT_DS, "seq_for_mut_prob.tsv"),
897
+ organism="Homo sapiens",
898
+ mane=True,
899
+ num_cores=8,
900
+ mane_version=1.3)