FunVIP 0.3.20__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.
- FunVIP-0.3.20.dist-info/LICENSE +674 -0
- FunVIP-0.3.20.dist-info/METADATA +32 -0
- FunVIP-0.3.20.dist-info/RECORD +36 -0
- FunVIP-0.3.20.dist-info/WHEEL +5 -0
- FunVIP-0.3.20.dist-info/entry_points.txt +3 -0
- FunVIP-0.3.20.dist-info/top_level.txt +3 -0
- data/__init__.py +0 -0
- external/BLAST_Windows/bin/cleanup-blastdb-volumes.py +162 -0
- external/__init__.py +0 -0
- src/__init__.py +0 -0
- src/align.py +124 -0
- src/cluster.py +510 -0
- src/command.py +360 -0
- src/concatenate.py +356 -0
- src/dataset.py +716 -0
- src/ext.py +448 -0
- src/hasher.py +98 -0
- src/initialize.py +335 -0
- src/logger.py +69 -0
- src/logics.py +104 -0
- src/modeltest.py +443 -0
- src/ncbi.py +160 -0
- src/opt_generator.py +72 -0
- src/patch.py +261 -0
- src/reporter.py +875 -0
- src/save.py +181 -0
- src/search.py +440 -0
- src/tool.py +309 -0
- src/tree.py +222 -0
- src/tree_interpretation.py +1379 -0
- src/tree_interpretation_pipe.py +679 -0
- src/trim.py +118 -0
- src/validate_input.py +846 -0
- src/validate_option.py +1609 -0
- src/validation.py +38 -0
- src/version.py +337 -0
src/validate_input.py
ADDED
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import shutil
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import json
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import numpy as np
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
|
|
13
|
+
from unidecode import unidecode
|
|
14
|
+
from Bio import SeqIO
|
|
15
|
+
from funvip.src.tool import (
|
|
16
|
+
initialize_path,
|
|
17
|
+
get_genus_species,
|
|
18
|
+
get_id,
|
|
19
|
+
manage_unicode,
|
|
20
|
+
mkdir,
|
|
21
|
+
)
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from funvip.src import save
|
|
25
|
+
from funvip.src.logics import isnewicklegal, isuniquecolumn, isvalidcolor
|
|
26
|
+
from funvip.src.hasher import decode, newick_legal, hash_funinfo_list
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## newick illegal characters
|
|
30
|
+
# fmt: off
|
|
31
|
+
NEWICK_ILLEGAL = ("(",'"',"[",":",";","/","[","]","{","}","(",")",",","]","+",'"',")"," ",)
|
|
32
|
+
# fmt: on
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Default funinfo class
|
|
36
|
+
# Abbreviated as FI in most of the codes
|
|
37
|
+
class Funinfo:
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.original_id = "" # original id, can be newick illegal
|
|
40
|
+
self.id = "" # newick illegal characters removed
|
|
41
|
+
self.hash = "" # hash : HSXXHE
|
|
42
|
+
self.description = "" # full description from fasta
|
|
43
|
+
self.ori_genus = "" # original genus
|
|
44
|
+
self.genus = "" # final genus
|
|
45
|
+
self.ori_species = "" # original species
|
|
46
|
+
self.bygene_species = {"concatenated": ""} # species name designated by gene
|
|
47
|
+
self.final_species = "" # final species designated by concatenated analysis
|
|
48
|
+
# species identifier if multiple branches with same species exists - ambiguous in result
|
|
49
|
+
self.species_identifier = 0
|
|
50
|
+
self.source = ""
|
|
51
|
+
self.datatype = "" # DB or Query
|
|
52
|
+
self.group = "" # original taxonomic group
|
|
53
|
+
self.adjusted_group = "" # adjusted taxonomic group by group clustering
|
|
54
|
+
self.seq = {}
|
|
55
|
+
self.unclassified_seq = []
|
|
56
|
+
self.color = None # color for highlighting in phylogenetic tree
|
|
57
|
+
self.flat = [] # list of flat species in concatenated tree
|
|
58
|
+
self.issues = set() # list of issues to this FI
|
|
59
|
+
|
|
60
|
+
def update_seqrecord(self, seq, gene=None):
|
|
61
|
+
flag = 0
|
|
62
|
+
self.description = seq.description
|
|
63
|
+
self.genus, self.ori_species = get_genus_species(seq.description)
|
|
64
|
+
|
|
65
|
+
if gene in self.seq:
|
|
66
|
+
logging.error(
|
|
67
|
+
f"More than 1 sequence for {gene} found for {self.id} during update_seqrecord"
|
|
68
|
+
)
|
|
69
|
+
flag = -1
|
|
70
|
+
elif gene is None:
|
|
71
|
+
self.unclassified_seq.append(str(seq.seq.ungap("-")))
|
|
72
|
+
else:
|
|
73
|
+
self.seq[gene] = str(seq.seq.ungap("-"))
|
|
74
|
+
|
|
75
|
+
self.bygene_species[gene] = self.ori_species
|
|
76
|
+
|
|
77
|
+
return flag
|
|
78
|
+
|
|
79
|
+
def update_seq(self, gene, seq): # get input as Entrez seqrecord! Important!
|
|
80
|
+
flag = 0
|
|
81
|
+
if gene in self.seq:
|
|
82
|
+
if (
|
|
83
|
+
self.seq[gene] != seq
|
|
84
|
+
): # if more than 1 sequence per gene gets in, and if they are different
|
|
85
|
+
logging.error(
|
|
86
|
+
f"More than 1 sequence for {gene} found for {self.id} during update_seq"
|
|
87
|
+
)
|
|
88
|
+
flag = -1
|
|
89
|
+
else:
|
|
90
|
+
pass
|
|
91
|
+
else:
|
|
92
|
+
self.seq[gene] = seq
|
|
93
|
+
|
|
94
|
+
self.bygene_species[gene] = self.ori_species
|
|
95
|
+
|
|
96
|
+
# Update concatenated
|
|
97
|
+
self.bygene_species["concatenated"] = self.ori_species
|
|
98
|
+
|
|
99
|
+
return flag
|
|
100
|
+
|
|
101
|
+
def update_description(self, description):
|
|
102
|
+
self.description = description
|
|
103
|
+
|
|
104
|
+
def update_genus(self, genus):
|
|
105
|
+
flag = 0
|
|
106
|
+
# Try to solve illegal unicode characters
|
|
107
|
+
if pd.isnull(genus):
|
|
108
|
+
genus = ""
|
|
109
|
+
|
|
110
|
+
# Genus with space causes error while mafft
|
|
111
|
+
# Genus with slash causes error while tree construction
|
|
112
|
+
genus = genus.strip().replace(" ", "_").replace("/", "_")
|
|
113
|
+
genus = manage_unicode(genus, column="Genus")
|
|
114
|
+
|
|
115
|
+
# Check ambiguity
|
|
116
|
+
if self.genus != "" and self.genus != genus:
|
|
117
|
+
flag = -1
|
|
118
|
+
logging.error(
|
|
119
|
+
f"Colliding genus info found for {self.original_id}, {self.genus} and {genus}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Update original if should
|
|
123
|
+
if self.ori_genus == "":
|
|
124
|
+
self.ori_genus = genus
|
|
125
|
+
|
|
126
|
+
# Update genus
|
|
127
|
+
self.genus = genus
|
|
128
|
+
|
|
129
|
+
return flag
|
|
130
|
+
|
|
131
|
+
def update_ori_species(self, species):
|
|
132
|
+
flag = 0
|
|
133
|
+
# Try to solve illegal unicode characters
|
|
134
|
+
if pd.isnull(species):
|
|
135
|
+
species = ""
|
|
136
|
+
|
|
137
|
+
# Genus with space causes error while mafft
|
|
138
|
+
# Genus with slash causes error while tree construction
|
|
139
|
+
species = species.strip().replace(" ", "_").replace("/", "_")
|
|
140
|
+
species = manage_unicode(species, column="Species")
|
|
141
|
+
|
|
142
|
+
# Check ambiguity
|
|
143
|
+
if self.ori_species != "" and self.ori_species != species:
|
|
144
|
+
flag = -1
|
|
145
|
+
logging.error(
|
|
146
|
+
f"Colliding species info found for {self.original_id}, {self.ori_species} and {species}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Update original if should
|
|
150
|
+
if self.ori_species == "":
|
|
151
|
+
self.ori_species = species
|
|
152
|
+
|
|
153
|
+
return flag
|
|
154
|
+
|
|
155
|
+
def update_species(self, gene, species):
|
|
156
|
+
self.bygene_species[gene] = species
|
|
157
|
+
|
|
158
|
+
def update_group(self, group):
|
|
159
|
+
flag = 0
|
|
160
|
+
# Try to solve illegal unicode characters
|
|
161
|
+
if pd.isnull(group):
|
|
162
|
+
group = ""
|
|
163
|
+
|
|
164
|
+
# Group with space causes error while mafft
|
|
165
|
+
group = group.strip().replace(" ", "_")
|
|
166
|
+
group = manage_unicode(group, column="Group")
|
|
167
|
+
|
|
168
|
+
# Check ambiguity
|
|
169
|
+
if self.group != "" and self.group != group:
|
|
170
|
+
logging.error(
|
|
171
|
+
f"Colliding group info found for {self.original_id}, {self.group} and {group}"
|
|
172
|
+
)
|
|
173
|
+
flag = -1
|
|
174
|
+
|
|
175
|
+
# Update group
|
|
176
|
+
self.group = group
|
|
177
|
+
|
|
178
|
+
return flag
|
|
179
|
+
|
|
180
|
+
def update_color(self, color):
|
|
181
|
+
if pd.isnull(color):
|
|
182
|
+
color = None
|
|
183
|
+
self.color = color
|
|
184
|
+
else:
|
|
185
|
+
color = manage_unicode(str(color).strip(), column="Color")
|
|
186
|
+
if isvalidcolor(color) is True:
|
|
187
|
+
self.color = color
|
|
188
|
+
else:
|
|
189
|
+
logging.error(
|
|
190
|
+
f"Color {color} does not seems to be valid svg color nor hex code"
|
|
191
|
+
)
|
|
192
|
+
raise Exception
|
|
193
|
+
|
|
194
|
+
def update_datatype(self, datatype):
|
|
195
|
+
flag = 0
|
|
196
|
+
# Available datatypes : db, query
|
|
197
|
+
if not (datatype in ("db", "query", "outgroup")):
|
|
198
|
+
logging.error(f"DEVELOPMENTAL ERROR: {datatype} is not available datatype")
|
|
199
|
+
raise Exception
|
|
200
|
+
|
|
201
|
+
# Check ambiguity
|
|
202
|
+
if self.datatype != "" and self.datatype != datatype:
|
|
203
|
+
flag = -1
|
|
204
|
+
logging.error(
|
|
205
|
+
f"Colliding datatype found for {self.original_id}, {self.datatype} and {datatype}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
self.datatype = datatype
|
|
209
|
+
|
|
210
|
+
return flag
|
|
211
|
+
|
|
212
|
+
def update_id(self, id_, regexs=None):
|
|
213
|
+
if not regexs == None:
|
|
214
|
+
id_ = get_id(id_, tuple(regexs))
|
|
215
|
+
|
|
216
|
+
# if cannot find id by regex
|
|
217
|
+
# Even for original, new line character makes significant errors while working fasta, so remove it
|
|
218
|
+
id_ = str(id_).replace("\n", " ")
|
|
219
|
+
self.original_id = id_
|
|
220
|
+
id_ = newick_legal(id_)
|
|
221
|
+
self.id = id_
|
|
222
|
+
|
|
223
|
+
def update_hash(self, n):
|
|
224
|
+
self.hash = f"HS{n}HE"
|
|
225
|
+
|
|
226
|
+
def get_issue_str(self):
|
|
227
|
+
flat_issues = []
|
|
228
|
+
alignfail_issues = []
|
|
229
|
+
polyphyly_issues = []
|
|
230
|
+
other_issues = []
|
|
231
|
+
for issue in self.issues:
|
|
232
|
+
if issue.startswith("flat"):
|
|
233
|
+
flat_issues.append(issue)
|
|
234
|
+
elif issue.startswith("alignfail"):
|
|
235
|
+
alignfail_issues.append(issue)
|
|
236
|
+
elif issue.startswith("polyphyly"):
|
|
237
|
+
polyphyly_issues.append(issue)
|
|
238
|
+
else:
|
|
239
|
+
other_issues.append(issue)
|
|
240
|
+
|
|
241
|
+
issues_string = ", ".join(other_issues)
|
|
242
|
+
if len(flat_issues) > 0:
|
|
243
|
+
flat_issue_str = "flat:" + "/".join(
|
|
244
|
+
sorted([issue.split(":")[1] for issue in flat_issues])
|
|
245
|
+
)
|
|
246
|
+
if len(issues_string) == 0:
|
|
247
|
+
issues_string = flat_issue_str
|
|
248
|
+
else:
|
|
249
|
+
issues_string += f", {flat_issue_str}"
|
|
250
|
+
|
|
251
|
+
if len(alignfail_issues) > 0:
|
|
252
|
+
alignfail_issue_str = "alignfail:" + "/".join(
|
|
253
|
+
sorted([issue.split(":")[1] for issue in alignfail_issues])
|
|
254
|
+
)
|
|
255
|
+
if len(issues_string) == 0:
|
|
256
|
+
issues_string = alignfail_issue_str
|
|
257
|
+
else:
|
|
258
|
+
issues_string += f", {alignfail_issue_str}"
|
|
259
|
+
|
|
260
|
+
if len(polyphyly_issues) > 0:
|
|
261
|
+
polyphyly_issue_str = "polyphyly:" + "/".join(
|
|
262
|
+
sorted([issue.split(":")[1] for issue in polyphyly_issues])
|
|
263
|
+
)
|
|
264
|
+
if len(issues_string) == 0:
|
|
265
|
+
issues_string = polyphyly_issue_str
|
|
266
|
+
else:
|
|
267
|
+
issues_string += f", {polyphyly_issue_str}"
|
|
268
|
+
|
|
269
|
+
return issues_string
|
|
270
|
+
|
|
271
|
+
def __repr__(self):
|
|
272
|
+
return f"FI: {self.id}"
|
|
273
|
+
|
|
274
|
+
def __hash__(self):
|
|
275
|
+
return hash((self.original_id, self.hash, self.description))
|
|
276
|
+
|
|
277
|
+
def __eq__(self, other):
|
|
278
|
+
if not isinstance(other, type(self)):
|
|
279
|
+
return NotImplemented
|
|
280
|
+
return (
|
|
281
|
+
self.original_id == other.original_id
|
|
282
|
+
and self.hash == other.hash
|
|
283
|
+
and self.description == other.description
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# getting data input from fasta file
|
|
288
|
+
def input_fasta(path, opt, fasta_list, funinfo_dict, datatype):
|
|
289
|
+
# initialize path to use function "get_genus_species"
|
|
290
|
+
initialize_path(path)
|
|
291
|
+
|
|
292
|
+
# Fasta files only
|
|
293
|
+
for file in fasta_list:
|
|
294
|
+
# Copy input files to designation
|
|
295
|
+
if datatype == "query":
|
|
296
|
+
shutil.copy(file, path.out_query)
|
|
297
|
+
elif datatype == "db":
|
|
298
|
+
shutil.copy(file, path.out_db)
|
|
299
|
+
|
|
300
|
+
tmp_list = []
|
|
301
|
+
|
|
302
|
+
full = ".".join(file.split(".")[:-1]) # full file path
|
|
303
|
+
name = ".".join(file.split("/")[-1].split(".")[:-1]) # name
|
|
304
|
+
|
|
305
|
+
logging.info(f"{file}: Fasta file")
|
|
306
|
+
|
|
307
|
+
error_flag = 0
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
seq_list = list(SeqIO.parse(file, "fasta"))
|
|
311
|
+
for seq in seq_list:
|
|
312
|
+
if not opt.regex == None:
|
|
313
|
+
id_ = get_id(seq.description, tuple(opt.regex))
|
|
314
|
+
else:
|
|
315
|
+
id_ = seq.description
|
|
316
|
+
|
|
317
|
+
id_ = newick_legal(id_)
|
|
318
|
+
|
|
319
|
+
if id_ in funinfo_dict:
|
|
320
|
+
error_flag += funinfo_dict[id_].update_seqrecord(seq)
|
|
321
|
+
error_flag += funinfo_dict[id_].update_datatype(datatype)
|
|
322
|
+
error_flag += funinfo_dict[id_].update_group("")
|
|
323
|
+
if get_genus_species(seq.description)[0] != "":
|
|
324
|
+
error_flag += funinfo_dict[id_].update_genus(
|
|
325
|
+
get_genus_species(seq.description)[0]
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if get_genus_species(seq.description)[1] != "":
|
|
329
|
+
error_flag += funinfo_dict[id_].update_ori_species(
|
|
330
|
+
get_genus_species(seq.description)[1]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# For new Funinfo
|
|
334
|
+
else:
|
|
335
|
+
newinfo = Funinfo()
|
|
336
|
+
error_flag += newinfo.update_seqrecord(seq)
|
|
337
|
+
error_flag += newinfo.update_datatype(datatype)
|
|
338
|
+
error_flag += newinfo.update_group(
|
|
339
|
+
""
|
|
340
|
+
) # because group not designated yet
|
|
341
|
+
# id by regex match
|
|
342
|
+
newinfo.update_id(seq.description, regexs=opt.regex)
|
|
343
|
+
if get_genus_species(seq.description)[0] != "":
|
|
344
|
+
error_flag += newinfo.update_genus(
|
|
345
|
+
get_genus_species(seq.description)[0]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if get_genus_species(seq.description)[1] != "":
|
|
349
|
+
error_flag += newinfo.update_ori_species(
|
|
350
|
+
get_genus_species(seq.description)[1]
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
funinfo_dict[id_] = deepcopy(newinfo)
|
|
354
|
+
|
|
355
|
+
except:
|
|
356
|
+
logging.warning(f"{file} does not seems to be valid fasta file skipping")
|
|
357
|
+
|
|
358
|
+
if len(seq_list) == 0:
|
|
359
|
+
logging.error(f"Fasta file {file} seems to be empty please check")
|
|
360
|
+
raise Exception
|
|
361
|
+
|
|
362
|
+
if error_flag < 0:
|
|
363
|
+
raise Exception
|
|
364
|
+
|
|
365
|
+
return funinfo_dict
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# getting datafile from excel or tabular file
|
|
369
|
+
def input_table(funinfo_dict, path, opt, table_list, datatype):
|
|
370
|
+
# Whether to check if GenMine has run
|
|
371
|
+
GenMine_flag = 0
|
|
372
|
+
string_error = 0
|
|
373
|
+
|
|
374
|
+
initialize_path(path) # this one is ugly
|
|
375
|
+
df_list = []
|
|
376
|
+
|
|
377
|
+
# extensionto filetype translation
|
|
378
|
+
dict_extension = {
|
|
379
|
+
".csv": "csv",
|
|
380
|
+
".tsv": "csv",
|
|
381
|
+
".xlsx": "excel",
|
|
382
|
+
".xls": "excel",
|
|
383
|
+
".parquet": "parquet",
|
|
384
|
+
".ftr": "feather",
|
|
385
|
+
".feather": "feather",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Running table by table operations
|
|
389
|
+
for table in table_list:
|
|
390
|
+
# Read each of the table by each of the extensions
|
|
391
|
+
flag_read_table = 0
|
|
392
|
+
for extension in dict_extension:
|
|
393
|
+
if table.endswith(extension):
|
|
394
|
+
try:
|
|
395
|
+
if dict_extension[extension] == "csv":
|
|
396
|
+
df = pd.read_csv(table)
|
|
397
|
+
flag_read_table = 1
|
|
398
|
+
elif dict_extension[extension] == "excel":
|
|
399
|
+
df = pd.read_excel(table)
|
|
400
|
+
flag_read_table = 1
|
|
401
|
+
elif dict_extension[extension] == "parquet":
|
|
402
|
+
df = pd.read_parquet(table, engine="pyarrow")
|
|
403
|
+
flag_read_table = 1
|
|
404
|
+
elif dict_extension[extension] == "feather":
|
|
405
|
+
df = pd.read_feather(table, use_threads=True)
|
|
406
|
+
flag_read_table = 1
|
|
407
|
+
|
|
408
|
+
# To prevent nan error
|
|
409
|
+
df = df.fillna("")
|
|
410
|
+
except:
|
|
411
|
+
logging.error(
|
|
412
|
+
f"Table {table} cannot be read as {dict_extension[extension]} file. Please check files, extensions and seperators"
|
|
413
|
+
)
|
|
414
|
+
raise Exception
|
|
415
|
+
|
|
416
|
+
if flag_read_table == 0:
|
|
417
|
+
logging.error(
|
|
418
|
+
f"Table {table} cannot recognized as either csv, xlsx, feather or parquet. Please check if extensions endswith .csv, .tsv, .xlsx, .parquet, .ftr or .feather"
|
|
419
|
+
)
|
|
420
|
+
raise Exception
|
|
421
|
+
|
|
422
|
+
# Lower case column names
|
|
423
|
+
df.columns = df.columns.str.lower()
|
|
424
|
+
df.columns = df.columns.str.strip()
|
|
425
|
+
df_list.append(df)
|
|
426
|
+
|
|
427
|
+
# Clean up columns
|
|
428
|
+
# Check if "id" column exists and unique
|
|
429
|
+
flag_id = isuniquecolumn(
|
|
430
|
+
list_column=list(df.columns), column=("accession", "id"), table_name=table
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# If old accession column, change to id
|
|
434
|
+
if flag_id == "accession":
|
|
435
|
+
df.rename(columns={"accession": "id"}, inplace=True)
|
|
436
|
+
|
|
437
|
+
# Check if "genus" column exists and unique
|
|
438
|
+
# Column "genus" is mandatory in db, and optional in query
|
|
439
|
+
check_none = True if datatype == "db" else False
|
|
440
|
+
flag_genus = isuniquecolumn(
|
|
441
|
+
list_column=list(df.columns),
|
|
442
|
+
column=tuple(("genus",)),
|
|
443
|
+
table_name=table,
|
|
444
|
+
check_none=check_none,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Check if "species" column exists and unique
|
|
448
|
+
# Column "species" is mandatory in db, and optional in query
|
|
449
|
+
check_none = True if datatype == "db" else False
|
|
450
|
+
flag_species = isuniquecolumn(
|
|
451
|
+
list_column=list(df.columns),
|
|
452
|
+
column=tuple(("species",)),
|
|
453
|
+
table_name=table,
|
|
454
|
+
check_none=check_none,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Check "opt.level" column exists and unique
|
|
458
|
+
# Column "opt.level" is mandatory in db, and optional inquery
|
|
459
|
+
check_none = True if datatype == "db" else False
|
|
460
|
+
flag_level = isuniquecolumn(
|
|
461
|
+
list_column=list(df.columns),
|
|
462
|
+
column=tuple((opt.level,)),
|
|
463
|
+
table_name=table,
|
|
464
|
+
check_none=check_none,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Check column color
|
|
468
|
+
# color column is optional
|
|
469
|
+
flag_color = isuniquecolumn(
|
|
470
|
+
list_column=list(df.columns),
|
|
471
|
+
column=tuple(("color",)),
|
|
472
|
+
table_name=table,
|
|
473
|
+
check_none=False,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Sequence column operations, download sequences with GenMine
|
|
477
|
+
if 1: # If download on/off option added, change this part
|
|
478
|
+
download_dict = {} # for downloaded sequences
|
|
479
|
+
download_set = set()
|
|
480
|
+
# 1 letter + 5 digit regex should be last, because they overlap with 2 letter + 6 digit ids / shotgun
|
|
481
|
+
regex_genbank = r"(([A-Z]{1}[0-9]{5})(\.[0-9]{1}){0,1})|(([A-Z]{2}[\_]{0,1}[0-9]{6}){1}([\.][0-9]){0,1})|(([A-Z]{4}[0-9]{8})(\.[0-9]{1}){0,1})|(([A-Z]{6}[0-9]{9,})(\.[0-9]{1}){0,1})"
|
|
482
|
+
|
|
483
|
+
# if gene name were not designated by user, use seq
|
|
484
|
+
opt.gene = list(set([gene.lower().strip() for gene in opt.gene]))
|
|
485
|
+
|
|
486
|
+
# find all NCBI accessions in seq
|
|
487
|
+
for gene in opt.gene:
|
|
488
|
+
if isuniquecolumn(
|
|
489
|
+
list_column=df.columns,
|
|
490
|
+
column=tuple((gene,)),
|
|
491
|
+
table_name=table,
|
|
492
|
+
check_none=False,
|
|
493
|
+
):
|
|
494
|
+
for n, _ in enumerate(df[gene]):
|
|
495
|
+
if not (pd.isna(df[gene][n])):
|
|
496
|
+
if re.search(regex_genbank, df[gene][n]):
|
|
497
|
+
# remove unexpected indents with strip
|
|
498
|
+
download_set.add(df[gene][n].strip())
|
|
499
|
+
|
|
500
|
+
# if NCBI accessions detected in sequence part, download it
|
|
501
|
+
if len(download_set) > 0:
|
|
502
|
+
GenMine_flag = 1
|
|
503
|
+
if opt.email == "":
|
|
504
|
+
logging.error(
|
|
505
|
+
"Your database includes GenBank accession but email not provided. --email is required to connect to GenBank"
|
|
506
|
+
)
|
|
507
|
+
raise Exception
|
|
508
|
+
|
|
509
|
+
logging.info(
|
|
510
|
+
f"Running GenMine to download {len(download_set)} sequences from GenBank"
|
|
511
|
+
)
|
|
512
|
+
# logging.info(download_set)
|
|
513
|
+
|
|
514
|
+
# Write GenMine input file
|
|
515
|
+
with open(f"{path.GenMine}/Accessions.txt", "w") as fg:
|
|
516
|
+
for acc in download_set:
|
|
517
|
+
fg.write(f"{acc.strip()}\n")
|
|
518
|
+
|
|
519
|
+
# Run GenMine
|
|
520
|
+
accession_path = f"{path.GenMine}/Accessions.txt"
|
|
521
|
+
# To prevent space errors in windows
|
|
522
|
+
if " " in path.GenMine:
|
|
523
|
+
accession_path = f'"{accession_path}"'
|
|
524
|
+
GenMine_path = f'"{path.GenMine}"'
|
|
525
|
+
else:
|
|
526
|
+
GenMine_path = path.GenMine
|
|
527
|
+
|
|
528
|
+
cmd = f"GenMine -c {accession_path} -o {GenMine_path} -e {opt.email}"
|
|
529
|
+
logging.info(cmd)
|
|
530
|
+
|
|
531
|
+
return_code = subprocess.call(cmd, shell=True)
|
|
532
|
+
|
|
533
|
+
if return_code != 0:
|
|
534
|
+
logging.error(f"GenMine failed with return_code: {return_code}")
|
|
535
|
+
logging.error(f"This is usually NCBI server connection error")
|
|
536
|
+
logging.error(
|
|
537
|
+
f"Check your network problems, or replace all accession numbers in your db file with actual sequences to run locally"
|
|
538
|
+
)
|
|
539
|
+
raise Exception
|
|
540
|
+
|
|
541
|
+
GenMine_df_list = [
|
|
542
|
+
file
|
|
543
|
+
for file in os.listdir(path.GenMine)
|
|
544
|
+
if file.endswith("_result.xlsx")
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
if len(GenMine_df_list) == 1:
|
|
548
|
+
download_df = pd.read_excel(f"{path.GenMine}/{GenMine_df_list[0]}")
|
|
549
|
+
|
|
550
|
+
# Generate download_dict (I think this can be done with pandas operation, but a bit tricky. Will be done later)
|
|
551
|
+
for n, acc in enumerate(download_df["acc"]):
|
|
552
|
+
download_dict[acc.strip()] = download_df["seq"][n]
|
|
553
|
+
|
|
554
|
+
# replace accession to sequence downloaded
|
|
555
|
+
def update_from_GenMine(string):
|
|
556
|
+
string = str(string)
|
|
557
|
+
accession_wo_version = string.strip().split(".")[0]
|
|
558
|
+
accession_w_version = string.strip()
|
|
559
|
+
|
|
560
|
+
# Do not consider sequence version
|
|
561
|
+
if accession_wo_version in download_dict:
|
|
562
|
+
return download_dict[accession_wo_version]
|
|
563
|
+
|
|
564
|
+
# Remove accessions which failed download to prevent conflict with sequence validation
|
|
565
|
+
elif not (accession_wo_version in download_dict) and (
|
|
566
|
+
accession_w_version in download_set
|
|
567
|
+
):
|
|
568
|
+
logging.warning(f"Failed updating {string}")
|
|
569
|
+
return ""
|
|
570
|
+
|
|
571
|
+
# For sequence input
|
|
572
|
+
else:
|
|
573
|
+
return string
|
|
574
|
+
|
|
575
|
+
for gene in opt.gene:
|
|
576
|
+
if gene in df.columns:
|
|
577
|
+
df[gene] = df[gene].apply(update_from_GenMine)
|
|
578
|
+
|
|
579
|
+
# Remove GenMine results to prevent collision with next set
|
|
580
|
+
for file in os.listdir(path.GenMine):
|
|
581
|
+
if "_result.xlsx" in file:
|
|
582
|
+
os.remove(f"{path.GenMine}/{file}")
|
|
583
|
+
|
|
584
|
+
elif len(GenMine_df_list) == 0:
|
|
585
|
+
logging.warning(
|
|
586
|
+
f"None of the GenMine results were succesfully parsed"
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
logging.error(
|
|
590
|
+
f"DEVELOPMENTAL ERROR: Multiple GenMine result colliding!"
|
|
591
|
+
)
|
|
592
|
+
raise Exception
|
|
593
|
+
|
|
594
|
+
error_flag = 0
|
|
595
|
+
|
|
596
|
+
# Manage unicode for ID
|
|
597
|
+
df["id"] = df["id"].apply(
|
|
598
|
+
lambda x: manage_unicode(str(x), column="ID/Accession")
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# To prevent errors on genus / spcies column
|
|
602
|
+
# if this function operates with query mode, there might be no genus or species column
|
|
603
|
+
if "genus" in df.columns:
|
|
604
|
+
df["genus"] = df["genus"].apply(lambda x: x.replace(" ", "_"))
|
|
605
|
+
df["genus"] = df["genus"].apply(lambda x: x.replace("/", "_"))
|
|
606
|
+
if "species" in df.columns:
|
|
607
|
+
df["species"] = df["species"].apply(lambda x: x.replace(" ", "_"))
|
|
608
|
+
df["species"] = df["species"].apply(lambda x: x.replace("/", "_"))
|
|
609
|
+
|
|
610
|
+
# Empty id check
|
|
611
|
+
empty_error = []
|
|
612
|
+
for n, acc in enumerate(df["id"]):
|
|
613
|
+
if df["id"][n].strip() == "" or df["id"][n].strip() == "-":
|
|
614
|
+
empty_error.append(n)
|
|
615
|
+
|
|
616
|
+
if len(empty_error) > 0:
|
|
617
|
+
logging.error(f"Empty id found in {table}, line {empty_error}!")
|
|
618
|
+
raise Exception
|
|
619
|
+
|
|
620
|
+
# Generate funinfo by each row
|
|
621
|
+
for n, acc in enumerate(df["id"]):
|
|
622
|
+
# Check if each of the ids are unique
|
|
623
|
+
# Remove non-unicode first
|
|
624
|
+
new_acc = True
|
|
625
|
+
|
|
626
|
+
# Generate funinfo for each id
|
|
627
|
+
# Duplicate id check
|
|
628
|
+
if df["id"][n] in funinfo_dict:
|
|
629
|
+
newinfo = funinfo_dict[df["id"][n]]
|
|
630
|
+
new_acc = False
|
|
631
|
+
logging.warning(f"Duplicate id {df['id'][n]} found!")
|
|
632
|
+
else:
|
|
633
|
+
funinfo_dict[df["id"][n]] = Funinfo()
|
|
634
|
+
newinfo = funinfo_dict[df["id"][n]]
|
|
635
|
+
newinfo.update_id(df["id"][n])
|
|
636
|
+
|
|
637
|
+
# if flag_genus is true, try to parse genus
|
|
638
|
+
if not (flag_genus is None or flag_genus is False):
|
|
639
|
+
error_flag += newinfo.update_genus(df["genus"][n])
|
|
640
|
+
|
|
641
|
+
# if flag_species is true, try to parse species
|
|
642
|
+
if not (flag_species is None or flag_species is False):
|
|
643
|
+
error_flag += newinfo.update_ori_species(df["species"][n])
|
|
644
|
+
|
|
645
|
+
# if flag_level is true, try to parse the optimal taxonomic group
|
|
646
|
+
if not (flag_level is None or flag_level is False):
|
|
647
|
+
error_flag += newinfo.update_group(df[flag_level][n])
|
|
648
|
+
|
|
649
|
+
# if flag_color is true, try to parse color for taxon
|
|
650
|
+
if not (flag_color is None or flag_color is False):
|
|
651
|
+
newinfo.update_color(df[flag_color][n])
|
|
652
|
+
|
|
653
|
+
# update datatype
|
|
654
|
+
error_flag += newinfo.update_datatype(datatype)
|
|
655
|
+
|
|
656
|
+
# parse each of the genes
|
|
657
|
+
for gene in opt.gene:
|
|
658
|
+
seq_error = 0
|
|
659
|
+
if gene in df.columns:
|
|
660
|
+
if not (pd.isna(df[gene][n])) or str(df[gene][n]).strip() == "":
|
|
661
|
+
# skip blank sequences
|
|
662
|
+
if df[gene][n].startswith(">"):
|
|
663
|
+
# remove fasta header
|
|
664
|
+
seq_string = "".join(df[gene][n].split("\n")[1:])
|
|
665
|
+
else:
|
|
666
|
+
seq_string = df[gene][n]
|
|
667
|
+
|
|
668
|
+
# adjust seq_string
|
|
669
|
+
seq_string = seq_string.replace("\n", "").replace(" ", "")
|
|
670
|
+
|
|
671
|
+
# Finding if DNA sequence contains error
|
|
672
|
+
seq_error_cnt = 0
|
|
673
|
+
seq_error_list = []
|
|
674
|
+
|
|
675
|
+
seq_string = manage_unicode(seq_string)
|
|
676
|
+
for x in seq_string: # x is every character of sequence
|
|
677
|
+
if not x.lower() in "acgtryswkmbdhvn-.":
|
|
678
|
+
seq_error_cnt += 1
|
|
679
|
+
seq_error_list.append(x)
|
|
680
|
+
|
|
681
|
+
if seq_error_cnt > 0:
|
|
682
|
+
logging.warning(
|
|
683
|
+
f"Illegal DNA character {seq_error_list} found in {gene} of {datatype} {df['id'][n]}"
|
|
684
|
+
)
|
|
685
|
+
elif seq_string.lower().strip() in ("nan", "na"):
|
|
686
|
+
logging.warning(
|
|
687
|
+
f"Sequence {df['id'][n]} {seq_string} detected as nan, removing it"
|
|
688
|
+
)
|
|
689
|
+
elif seq_error_cnt == 0:
|
|
690
|
+
# remove gaps for preventing BLAST error
|
|
691
|
+
error_flag += newinfo.update_seq(
|
|
692
|
+
gene, seq_string.replace("-", "").replace(".", "")
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if error_flag < 0:
|
|
696
|
+
raise Exception
|
|
697
|
+
|
|
698
|
+
# After successfully parsed this table, save it
|
|
699
|
+
save.save_df(
|
|
700
|
+
df,
|
|
701
|
+
f"{path.out_db}/Saved_{'.'.join(table.split('/')[-1].split('.')[:-1])}.{opt.tableformat}",
|
|
702
|
+
fmt=opt.tableformat,
|
|
703
|
+
)
|
|
704
|
+
return funinfo_dict, GenMine_flag
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def db_input(funinfo_dict, opt, path) -> list:
|
|
708
|
+
# Get DB input
|
|
709
|
+
logging.info(f"Input DB list: {opt.db}")
|
|
710
|
+
|
|
711
|
+
funinfo_dict, GenMine_flag = input_table(
|
|
712
|
+
funinfo_dict=funinfo_dict, path=path, opt=opt, table_list=opt.db, datatype="db"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# validate dataset
|
|
716
|
+
# if only one group exists, outgroup cannot work
|
|
717
|
+
group_set = set(funinfo_dict[key].group for key in funinfo_dict)
|
|
718
|
+
group_set.discard("")
|
|
719
|
+
if len(group_set) <= 1:
|
|
720
|
+
logging.error(
|
|
721
|
+
f"Only 1 group soley detected : {group_set}. Please add outgroup sequences"
|
|
722
|
+
)
|
|
723
|
+
raise Exception
|
|
724
|
+
|
|
725
|
+
# check if minimum outgroup number count exceeds minimum group
|
|
726
|
+
group_cnt_dict = {x: 0 for x in group_set}
|
|
727
|
+
for key in funinfo_dict:
|
|
728
|
+
if type(funinfo_dict[key].group) is str:
|
|
729
|
+
if funinfo_dict[key].group in group_set:
|
|
730
|
+
group_cnt_dict[funinfo_dict[key].group] += 1
|
|
731
|
+
|
|
732
|
+
if any(group_cnt_dict[x] < opt.maxoutgroup for x in group_cnt_dict):
|
|
733
|
+
logging.warning(
|
|
734
|
+
f"Sequences in database of some group has lower number than MINIMUM_OUTGROUP_COUNT. It may cause error when outgroup selection, or may select not most appropriate outgroup to group. Please lower number of MINIMUM_OUTGROUP_COUNT in option or add more sequences to these groups"
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
return funinfo_dict, GenMine_flag
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def query_input(funinfo_dict, opt, path):
|
|
741
|
+
query_fasta = [
|
|
742
|
+
file
|
|
743
|
+
for file in opt.query
|
|
744
|
+
if any(file.endswith(x) for x in (".fa", ".fna", ".fas", ".fasta", ".txt"))
|
|
745
|
+
]
|
|
746
|
+
query_table = [
|
|
747
|
+
file
|
|
748
|
+
for file in opt.query
|
|
749
|
+
if any(
|
|
750
|
+
file.endswith(x)
|
|
751
|
+
for x in (".csv", ".tsv", ".xlsx", ".ftr", ".feather", ".parquet")
|
|
752
|
+
)
|
|
753
|
+
]
|
|
754
|
+
|
|
755
|
+
funinfo_dict, GenMine_flag = input_table(
|
|
756
|
+
funinfo_dict=funinfo_dict,
|
|
757
|
+
path=path,
|
|
758
|
+
opt=opt,
|
|
759
|
+
table_list=query_table,
|
|
760
|
+
datatype="query",
|
|
761
|
+
)
|
|
762
|
+
funinfo_dict = input_fasta(
|
|
763
|
+
path=path,
|
|
764
|
+
opt=opt,
|
|
765
|
+
fasta_list=query_fasta,
|
|
766
|
+
funinfo_dict=funinfo_dict,
|
|
767
|
+
datatype="query",
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
# Save query initially in raw format, because gene has not been assigned yet
|
|
771
|
+
for file in query_table:
|
|
772
|
+
shutil.copy(f"{file}", f"{path.out_query}")
|
|
773
|
+
|
|
774
|
+
for file in query_fasta:
|
|
775
|
+
shutil.copy(f"{file}", f"{path.out_query}")
|
|
776
|
+
|
|
777
|
+
logging.info(
|
|
778
|
+
f"Total {len([funinfo_dict[key].datatype =='query' for key in funinfo_dict.keys()])} sequences parsed from query"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
return funinfo_dict, GenMine_flag
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# combined db and query input
|
|
785
|
+
def data_input(V, R, opt, path):
|
|
786
|
+
funinfo_dict = {}
|
|
787
|
+
|
|
788
|
+
# get database input
|
|
789
|
+
funinfo_dict, GenMine_flag_db = db_input(
|
|
790
|
+
funinfo_dict=funinfo_dict, opt=opt, path=path
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# get query input
|
|
794
|
+
funinfo_dict, GenMine_flag_query = query_input(
|
|
795
|
+
funinfo_dict=funinfo_dict, opt=opt, path=path
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
if GenMine_flag_db != 0 or GenMine_flag_query != 0:
|
|
799
|
+
GenMine_flag = 1
|
|
800
|
+
else:
|
|
801
|
+
GenMine_flag = 0
|
|
802
|
+
|
|
803
|
+
# combine all data
|
|
804
|
+
V.list_FI = [funinfo_dict[key] for key in funinfo_dict]
|
|
805
|
+
|
|
806
|
+
# hashing data for safety in tree analysis
|
|
807
|
+
V.list_FI = hash_funinfo_list(V.list_FI)
|
|
808
|
+
|
|
809
|
+
# make hash dict
|
|
810
|
+
for FI in V.list_FI:
|
|
811
|
+
V.dict_hash_FI[FI.hash] = FI
|
|
812
|
+
|
|
813
|
+
# If all genes are empty, indicate them
|
|
814
|
+
for FI in V.list_FI:
|
|
815
|
+
flag_available_gene = 0
|
|
816
|
+
for gene in FI.seq:
|
|
817
|
+
if FI.seq[gene] != "":
|
|
818
|
+
flag_available_gene = 1
|
|
819
|
+
|
|
820
|
+
if flag_available_gene == 0:
|
|
821
|
+
FI.issues.add("noseq")
|
|
822
|
+
|
|
823
|
+
return V, R, opt, GenMine_flag
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
# This function updates initial input query files to saved files with matrices and downloaded sequences
|
|
827
|
+
def update_queryfile(V, path, opt):
|
|
828
|
+
# Dictionary to generate dataframe
|
|
829
|
+
out_dict = {"ID": []}
|
|
830
|
+
|
|
831
|
+
for gene in opt.gene:
|
|
832
|
+
out_dict[gene] = []
|
|
833
|
+
|
|
834
|
+
for FI in V.list_FI:
|
|
835
|
+
if FI.datatype == "query":
|
|
836
|
+
out_dict["ID"].append(FI.original_id)
|
|
837
|
+
for gene in opt.gene:
|
|
838
|
+
if gene in FI.seq:
|
|
839
|
+
out_dict[gene].append(FI.seq[gene])
|
|
840
|
+
else:
|
|
841
|
+
out_dict[gene].append("")
|
|
842
|
+
|
|
843
|
+
df_out = pd.DataFrame(out_dict)
|
|
844
|
+
save.save_df(
|
|
845
|
+
df_out, f"{path.out_query}/Saved_query.{opt.tableformat}", fmt=opt.tableformat
|
|
846
|
+
)
|