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/dataset.py
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
from funvip.src import save
|
|
2
|
+
from funvip.src import hasher
|
|
3
|
+
from Bio import SeqIO
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import shutil
|
|
7
|
+
import numpy as np
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Datasets are group of sequences with same genes for analysis, including query, database and outgroup
|
|
14
|
+
# Each of the dataset varialbe
|
|
15
|
+
class Dataset:
|
|
16
|
+
def __init__(self, gene, group, list_qr, list_db, list_og):
|
|
17
|
+
# Which gene is this dataset, gene name or concatenated
|
|
18
|
+
self.gene = gene
|
|
19
|
+
# Which group is this dataset
|
|
20
|
+
self.group = group
|
|
21
|
+
# query funinfo_list for this dataset
|
|
22
|
+
self.list_qr_FI = list_qr
|
|
23
|
+
# db funinfo_list for this dataset
|
|
24
|
+
self.list_db_FI = list_db
|
|
25
|
+
# outgroup funinfo_list for this dataset
|
|
26
|
+
self.list_og_FI = list_og
|
|
27
|
+
|
|
28
|
+
def __repr__(self):
|
|
29
|
+
return f"<< FunVIP_Dataset object >> Gene: {self.gene} | Group: {self.group} | Query: {len(self.list_qr_FI)} | DB: {len(self.list_db_FI)} | Outgroup: {len(self.list_og_FI)}\n"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### FunVIP all dataset variables bundle
|
|
33
|
+
# Called as "V" in main pipeline
|
|
34
|
+
class FunVIP_var:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
# dataset class to manage datasets used for run
|
|
37
|
+
# FI : funinfo
|
|
38
|
+
# SR : search result
|
|
39
|
+
# prefix c : concatenated
|
|
40
|
+
# group : group
|
|
41
|
+
# dict_A_B : dictionary, {A : B}
|
|
42
|
+
# opt : list of option
|
|
43
|
+
# qr : query
|
|
44
|
+
# db : database
|
|
45
|
+
# og : outgroup
|
|
46
|
+
# rslt : result
|
|
47
|
+
|
|
48
|
+
# Funinfo related variables
|
|
49
|
+
# all funinfo_list, old funinfo_list
|
|
50
|
+
self.list_FI = []
|
|
51
|
+
# {funinfo.hash : funinfo}, old hash_dict
|
|
52
|
+
self.dict_hash_FI = {}
|
|
53
|
+
# {funinfo.hash : "{funinfo.id}_{funinfo.genus}_{funinfo.species}"}
|
|
54
|
+
self.dict_hash_name = {}
|
|
55
|
+
# {funinfo.hash : "{funinfo.id}"}
|
|
56
|
+
self.dict_hash_id = {}
|
|
57
|
+
|
|
58
|
+
# genus, gene - dataset
|
|
59
|
+
# tuple of genera used for current run, old genus_list
|
|
60
|
+
self.tup_genus = ()
|
|
61
|
+
# list of gene found from db in current run
|
|
62
|
+
self.list_db_gene = []
|
|
63
|
+
# list of gene used for query in current run
|
|
64
|
+
self.list_qr_gene = []
|
|
65
|
+
# old group list
|
|
66
|
+
self.list_group = []
|
|
67
|
+
# group dict -> {group:set(genus1, genus2, ... ) format}
|
|
68
|
+
# used in synchronizing to check if the genus is main interest of the group
|
|
69
|
+
self.dict_group = {}
|
|
70
|
+
|
|
71
|
+
# Search result datasets
|
|
72
|
+
# {gene : search df}, old df_dict
|
|
73
|
+
self.dict_gene_SR = {}
|
|
74
|
+
# concatenated search df dict, old concatenated_df
|
|
75
|
+
self.cSR = None
|
|
76
|
+
# {groupon : concatenated search df dict}, old concatenated_df in dictionary form by group
|
|
77
|
+
# self.dict_group_cSR = {}
|
|
78
|
+
|
|
79
|
+
# dataset dict - dict_dataset[group][gene] = class Dataset
|
|
80
|
+
# using key concat for concatenated
|
|
81
|
+
self.dict_dataset = {}
|
|
82
|
+
|
|
83
|
+
# Running options
|
|
84
|
+
# group clustering option
|
|
85
|
+
self.opt_cluster = []
|
|
86
|
+
self.opt_align = []
|
|
87
|
+
self.opt_tree = []
|
|
88
|
+
|
|
89
|
+
# Running result
|
|
90
|
+
self.rslt_cluster = []
|
|
91
|
+
self.rslt_align = []
|
|
92
|
+
self.rslt_tree = []
|
|
93
|
+
|
|
94
|
+
# multigene_list
|
|
95
|
+
self.multigene_list = []
|
|
96
|
+
|
|
97
|
+
# Partition information by dataset
|
|
98
|
+
# group : {"len" : dict, "order" : list} format
|
|
99
|
+
self.partition = {}
|
|
100
|
+
|
|
101
|
+
def __repr__(self):
|
|
102
|
+
out_dict = {}
|
|
103
|
+
for key in self.dict_dataset:
|
|
104
|
+
out_dict[key] = len(self.dict_dataset[key])
|
|
105
|
+
return f"<< FunVIP_var object >>\nNumber of FI: {len(self.list_FI)}\nDB genes:{self.list_db_gene}\nQuery genes:{self.list_qr_gene}\ngroups:{self.list_group}\nDataset_dict:{json.dumps(out_dict, indent=2)}"
|
|
106
|
+
|
|
107
|
+
def add_dataset(self, group, gene, list_qr, list_db, list_og):
|
|
108
|
+
data = Dataset(group, gene, list_qr, list_db, list_og)
|
|
109
|
+
if not (group) in self.dict_dataset:
|
|
110
|
+
self.dict_dataset[group] = {}
|
|
111
|
+
|
|
112
|
+
if not (gene) in self.dict_dataset[group]:
|
|
113
|
+
self.dict_dataset[group][gene] = data
|
|
114
|
+
else:
|
|
115
|
+
logging.warning(f"Overriding dataset on {group} {gene}")
|
|
116
|
+
self.dict_dataset[group][gene] = data
|
|
117
|
+
|
|
118
|
+
# If gene is concatenated, update ori_species
|
|
119
|
+
if gene == "concatenated":
|
|
120
|
+
for FI in list_qr + list_db + list_og:
|
|
121
|
+
FI.bygene_species[gene] = FI.ori_species
|
|
122
|
+
|
|
123
|
+
# Update dict_group
|
|
124
|
+
if not group in self.dict_group:
|
|
125
|
+
self.dict_group[group] = set()
|
|
126
|
+
|
|
127
|
+
for FI in list_db:
|
|
128
|
+
self.dict_group[group].add(FI.genus)
|
|
129
|
+
|
|
130
|
+
def remove_dataset(self, group, gene):
|
|
131
|
+
if not (group) in self.dict_dataset:
|
|
132
|
+
logging.warning(
|
|
133
|
+
f"Passing removing dataset of {group} {gene} because no {group} priorly exists"
|
|
134
|
+
)
|
|
135
|
+
elif not (gene) in self.dict_dataset[group]:
|
|
136
|
+
logging.warning(
|
|
137
|
+
f"Passing removing dataset of {group} {gene} because no {gene} priorly exists"
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
del self.dict_dataset[group][gene]
|
|
141
|
+
# if all gene removed
|
|
142
|
+
# 1 for concatenated
|
|
143
|
+
if len(self.dict_dataset[group]) <= 1:
|
|
144
|
+
del self.dict_dataset[group]
|
|
145
|
+
logging.info(f"Removed {group} from dataset")
|
|
146
|
+
else:
|
|
147
|
+
logging.info(f"Removed {group} {gene} from dataset")
|
|
148
|
+
|
|
149
|
+
# Check if given dict_dataset[group][gene] exists
|
|
150
|
+
def exist_dataset(self, group, gene):
|
|
151
|
+
try:
|
|
152
|
+
self.dict_dataset[group][gene]
|
|
153
|
+
return True
|
|
154
|
+
except:
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
# Check if dict_group has been properly generated
|
|
158
|
+
def check_dict_group(self, opt):
|
|
159
|
+
# If level is lower than genus, check if each group includes only one genus
|
|
160
|
+
if opt.level in ["subseries", "series", "subsection", "section", "genus"]:
|
|
161
|
+
for group in self.dict_group:
|
|
162
|
+
if len(self.dict_group[group]) > 1:
|
|
163
|
+
logging.warning(
|
|
164
|
+
f"{opt.level} {group} includes more than one genus, {self.dict_group[group]}.\n",
|
|
165
|
+
f"This may cause unusual behaviour in synchronizing",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# If level is higher than genus, check if each genus belongs to more than one group
|
|
169
|
+
elif opt.level in [
|
|
170
|
+
"subtribe",
|
|
171
|
+
"tribe",
|
|
172
|
+
"subfamily",
|
|
173
|
+
"family",
|
|
174
|
+
"suborder",
|
|
175
|
+
"order",
|
|
176
|
+
"subclass",
|
|
177
|
+
"class",
|
|
178
|
+
"subdivision",
|
|
179
|
+
"division",
|
|
180
|
+
"subphylum",
|
|
181
|
+
"phylum",
|
|
182
|
+
"subkingdom",
|
|
183
|
+
"kingdom",
|
|
184
|
+
]:
|
|
185
|
+
reverse_dict = {}
|
|
186
|
+
for group in self.dict_group:
|
|
187
|
+
for genus in self.dict_group[group]:
|
|
188
|
+
reverse_dict[genus] = set()
|
|
189
|
+
reverse_dict[genus].add(group)
|
|
190
|
+
|
|
191
|
+
for genus in reverse_dict:
|
|
192
|
+
if len(reverse_dict[genus]) > 1:
|
|
193
|
+
logging.warning(
|
|
194
|
+
f"genus {group} belongs to more than one {opt.level}, {reverse_dict[genus]}.\n",
|
|
195
|
+
f"This may cause unusual behaviour in synchronizing",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
logging.error(f"DEVELOPMENTAL ERROR, UNEXPECTED LEVEL {opt.level} selected")
|
|
200
|
+
raise Exception
|
|
201
|
+
|
|
202
|
+
# generate dataset by group and gene
|
|
203
|
+
def generate_dataset(self, opt):
|
|
204
|
+
# Format : dict_funinfo = {group: {gene : [FI]}}
|
|
205
|
+
dict_funinfo = {}
|
|
206
|
+
|
|
207
|
+
for group in self.list_group:
|
|
208
|
+
logging.info(f"Generating dataset for {group}")
|
|
209
|
+
dict_funinfo[group] = {}
|
|
210
|
+
|
|
211
|
+
# For queryonly case
|
|
212
|
+
if opt.queryonly is True:
|
|
213
|
+
# whether to run this group
|
|
214
|
+
group_flag = False
|
|
215
|
+
for gene in self.list_db_gene:
|
|
216
|
+
logging.debug(
|
|
217
|
+
f"Searching dataset {group} {gene} includes query sequences"
|
|
218
|
+
)
|
|
219
|
+
list_qr = [
|
|
220
|
+
FI
|
|
221
|
+
for FI in self.list_FI
|
|
222
|
+
if (
|
|
223
|
+
gene in FI.seq
|
|
224
|
+
and FI.datatype == "query"
|
|
225
|
+
and FI.adjusted_group == group
|
|
226
|
+
)
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# do not manage db when --queryonly True (--all False) and query does not exists
|
|
230
|
+
if len(list_qr) > 0:
|
|
231
|
+
group_flag = True
|
|
232
|
+
|
|
233
|
+
# if decided to run this group
|
|
234
|
+
if group_flag is True:
|
|
235
|
+
logging.info(f"Decided to construct dataset on {group}")
|
|
236
|
+
for gene in self.list_db_gene:
|
|
237
|
+
list_qr = [
|
|
238
|
+
FI
|
|
239
|
+
for FI in self.list_FI
|
|
240
|
+
if (
|
|
241
|
+
gene in FI.seq
|
|
242
|
+
and FI.datatype == "query"
|
|
243
|
+
and FI.adjusted_group == group
|
|
244
|
+
)
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
list_db = [
|
|
248
|
+
FI
|
|
249
|
+
for FI in self.list_FI
|
|
250
|
+
if (
|
|
251
|
+
gene in FI.seq
|
|
252
|
+
and FI.datatype == "db"
|
|
253
|
+
and FI.adjusted_group == group
|
|
254
|
+
)
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
self.add_dataset(group, gene, list_qr, list_db, [])
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
logging.warning(
|
|
261
|
+
f"group {group} did not passed dataset construction"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# for concatenated
|
|
265
|
+
list_qr = [
|
|
266
|
+
FI
|
|
267
|
+
for FI in self.list_FI
|
|
268
|
+
if (FI.datatype == "query" and FI.adjusted_group == group)
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# do not manage db when query only mode and query does not exists
|
|
272
|
+
if len(list_qr) > 0:
|
|
273
|
+
list_db = [
|
|
274
|
+
FI
|
|
275
|
+
for FI in self.list_FI
|
|
276
|
+
if (FI.datatype == "db" and FI.adjusted_group == group)
|
|
277
|
+
]
|
|
278
|
+
self.add_dataset(group, "concatenated", list_qr, list_db, [])
|
|
279
|
+
|
|
280
|
+
# For opt.queryonly is False -> run all dataset in database
|
|
281
|
+
else:
|
|
282
|
+
for gene in self.list_db_gene:
|
|
283
|
+
list_qr = [
|
|
284
|
+
FI
|
|
285
|
+
for FI in self.list_FI
|
|
286
|
+
if (
|
|
287
|
+
gene in FI.seq
|
|
288
|
+
and FI.datatype == "query"
|
|
289
|
+
and FI.adjusted_group == group
|
|
290
|
+
)
|
|
291
|
+
]
|
|
292
|
+
list_db = [
|
|
293
|
+
FI
|
|
294
|
+
for FI in self.list_FI
|
|
295
|
+
if (
|
|
296
|
+
gene in FI.seq
|
|
297
|
+
and FI.datatype == "db"
|
|
298
|
+
and FI.adjusted_group == group
|
|
299
|
+
)
|
|
300
|
+
]
|
|
301
|
+
self.add_dataset(group, gene, list_qr, list_db, [])
|
|
302
|
+
|
|
303
|
+
# for concatenated
|
|
304
|
+
list_qr = [
|
|
305
|
+
FI
|
|
306
|
+
for FI in self.list_FI
|
|
307
|
+
if (FI.datatype == "query" and FI.adjusted_group == group)
|
|
308
|
+
]
|
|
309
|
+
list_db = [
|
|
310
|
+
FI
|
|
311
|
+
for FI in self.list_FI
|
|
312
|
+
if (FI.datatype == "db" and FI.adjusted_group == group)
|
|
313
|
+
]
|
|
314
|
+
self.add_dataset(group, "concatenated", list_qr, list_db, [])
|
|
315
|
+
|
|
316
|
+
self.check_dict_group(opt)
|
|
317
|
+
|
|
318
|
+
# homogenize list_dataset and dict_hash_FI from multiple results
|
|
319
|
+
def homogenize_dataset(self):
|
|
320
|
+
for FI in self.list_FI:
|
|
321
|
+
if FI.hash in self.dict_hash_FI:
|
|
322
|
+
h = FI.hash
|
|
323
|
+
|
|
324
|
+
# final species
|
|
325
|
+
if FI.final_species != self.dict_hash_FI[h].final_species:
|
|
326
|
+
if FI.final_species == "":
|
|
327
|
+
FI.final_species = self.dict_hash_FI[h].final_species
|
|
328
|
+
elif self.dict_hash_FI[h].final_species == "":
|
|
329
|
+
self.dict_hash_FI[h].final_species = FI.final_species
|
|
330
|
+
else:
|
|
331
|
+
logging.error(
|
|
332
|
+
f"DEVELOPMNETAL ERROR Both list_FI and dict_hash_FI have conflicting final species, {FI.final_species} and {self.dict_hash_FI[h].final_species}"
|
|
333
|
+
)
|
|
334
|
+
raise Exception
|
|
335
|
+
|
|
336
|
+
# adjusted group
|
|
337
|
+
if FI.adjusted_group != self.dict_hash_FI[h].adjusted_group:
|
|
338
|
+
if FI.adjusted_group == "" or FI.adjusted_group == "":
|
|
339
|
+
FI.adjusted_group = self.dict_hash_FI[h].adjusted_group
|
|
340
|
+
elif self.dict_hash_FI[h].adjusted_group == "":
|
|
341
|
+
self.dict_hash_FI[h].adjusted_group = FI.adjusted_group
|
|
342
|
+
else:
|
|
343
|
+
logging.error(
|
|
344
|
+
f"DEVELOPMENTAL ERROR Both list_FI and dict_hash_FI have conflicting final group, {FI.adjusted_group} and {self.dict_hash_FI[h].adjusted_group}, {FI}"
|
|
345
|
+
)
|
|
346
|
+
raise Exception
|
|
347
|
+
|
|
348
|
+
elif (
|
|
349
|
+
FI.adjusted_group == ""
|
|
350
|
+
and self.dict_hash_FI[h].adjusted_group == ""
|
|
351
|
+
):
|
|
352
|
+
if FI.group != "":
|
|
353
|
+
FI.adjusted_group = FI.group
|
|
354
|
+
self.dict_hash_FI[h].adjusted_group = FI.group
|
|
355
|
+
|
|
356
|
+
elif self.dict_hash_FI[h].group != "":
|
|
357
|
+
FI.adjusted_group = self.dict_hash_FI[h].group
|
|
358
|
+
self.dict_hash_FI[h].adjusted_group = self.dict_hash_FI[h].group
|
|
359
|
+
else:
|
|
360
|
+
logging.warning(f"{FI.id} does not have assigned group!")
|
|
361
|
+
|
|
362
|
+
# bygene_species
|
|
363
|
+
if FI.bygene_species != self.dict_hash_FI[h].bygene_species:
|
|
364
|
+
# if both are empty
|
|
365
|
+
if not (FI.bygene_species) and not (
|
|
366
|
+
self.dict_hash_FI[h].bygene_species
|
|
367
|
+
):
|
|
368
|
+
pass
|
|
369
|
+
elif not (FI.bygene_species):
|
|
370
|
+
FI.bygene_species = self.dict_hash_FI[h].bygene_species
|
|
371
|
+
elif self.dict_hash_FI[h].bygene_species:
|
|
372
|
+
self.dict_hash_FI[h].bygene_species = FI.bygene_species
|
|
373
|
+
else:
|
|
374
|
+
logging.error(
|
|
375
|
+
f"DEVELOPMENTAL ERROR Both list_FI and dict_hash_FI have conflicting gene identification results, {FI.bygene_species} and {self.dict_hash_FI[h].bygene}"
|
|
376
|
+
)
|
|
377
|
+
raise Exception
|
|
378
|
+
|
|
379
|
+
# Remove invalid dataset to be analyzed
|
|
380
|
+
def remove_invalid_dataset(self):
|
|
381
|
+
# collect remove list : removing after iterating
|
|
382
|
+
list_remove = []
|
|
383
|
+
for group in self.dict_dataset:
|
|
384
|
+
for gene in self.dict_dataset[group]:
|
|
385
|
+
if len(self.dict_dataset[group][gene].list_db_FI) == 0:
|
|
386
|
+
logging.warning(
|
|
387
|
+
f"Removing {gene} from analysis in group {group} because there are no corresponding sequences"
|
|
388
|
+
)
|
|
389
|
+
list_remove.append((group, gene))
|
|
390
|
+
elif (
|
|
391
|
+
len(self.dict_dataset[group][gene].list_db_FI)
|
|
392
|
+
+ len(self.dict_dataset[group][gene].list_qr_FI)
|
|
393
|
+
+ len(self.dict_dataset[group][gene].list_og_FI)
|
|
394
|
+
< 4
|
|
395
|
+
):
|
|
396
|
+
logging.warning(
|
|
397
|
+
f"Removing {group} {gene} from downstream phylogenetic analysis because there are not enough sequences"
|
|
398
|
+
)
|
|
399
|
+
list_remove.append((group, gene))
|
|
400
|
+
|
|
401
|
+
for x in list_remove:
|
|
402
|
+
self.remove_dataset(*x)
|
|
403
|
+
|
|
404
|
+
# save fasta for outgroup adjusted fasta
|
|
405
|
+
def save_dataset(self, path, opt):
|
|
406
|
+
for group in self.dict_dataset:
|
|
407
|
+
for gene in self.dict_dataset[group]:
|
|
408
|
+
if not (gene == "concatenated"):
|
|
409
|
+
if "concatenated" in self.dict_dataset[group]:
|
|
410
|
+
fasta_list = list(
|
|
411
|
+
set(
|
|
412
|
+
self.dict_dataset[group][gene].list_db_FI
|
|
413
|
+
+ self.dict_dataset[group][gene].list_qr_FI
|
|
414
|
+
+ self.dict_dataset[group][gene].list_og_FI
|
|
415
|
+
+ self.dict_dataset[group]["concatenated"].list_og_FI
|
|
416
|
+
)
|
|
417
|
+
)
|
|
418
|
+
else:
|
|
419
|
+
fasta_list = (
|
|
420
|
+
self.dict_dataset[group][gene].list_db_FI
|
|
421
|
+
+ self.dict_dataset[group][gene].list_qr_FI
|
|
422
|
+
+ self.dict_dataset[group][gene].list_og_FI
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Remove no seqs
|
|
426
|
+
for fasta in fasta_list:
|
|
427
|
+
save.save_fasta(
|
|
428
|
+
fasta_list,
|
|
429
|
+
gene,
|
|
430
|
+
f"{path.out_adjusted}/{opt.runname}_Adjusted_{group}_{gene}.fasta",
|
|
431
|
+
by="hash",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Validate if any multiple sequence alignment has no overlapping region
|
|
435
|
+
def validate_alignments(self, path, opt):
|
|
436
|
+
fail_list = []
|
|
437
|
+
|
|
438
|
+
remove_dict = {}
|
|
439
|
+
tree_hash_dict = hasher.encode(self.list_FI, newick=True)
|
|
440
|
+
for group in self.dict_dataset:
|
|
441
|
+
remove_dict[group] = {}
|
|
442
|
+
for gene in self.dict_dataset[group]:
|
|
443
|
+
if gene != "concatenated":
|
|
444
|
+
remove_dict[group][gene] = []
|
|
445
|
+
# Check if alignment corresponding to dataset exists
|
|
446
|
+
if not (
|
|
447
|
+
os.path.isfile(
|
|
448
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta"
|
|
449
|
+
)
|
|
450
|
+
):
|
|
451
|
+
logger.warning(
|
|
452
|
+
f"Alignment file {path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta does not exists"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
fail_list.append(group_gene)
|
|
456
|
+
|
|
457
|
+
else:
|
|
458
|
+
# If alignment exists, check if alignment does have overlapping regions
|
|
459
|
+
## Parse alignment
|
|
460
|
+
seq_list = list(
|
|
461
|
+
SeqIO.parse(
|
|
462
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
|
|
463
|
+
"fasta",
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Remove sequences that has not been existed during alignment stage
|
|
468
|
+
seq_id_list = [seq.id for seq in seq_list]
|
|
469
|
+
for FI in self.dict_dataset[group][gene].list_db_FI:
|
|
470
|
+
if not (FI.hash in seq_id_list):
|
|
471
|
+
self.dict_dataset[group][gene].list_db_FI.remove(FI)
|
|
472
|
+
|
|
473
|
+
for FI in self.dict_dataset[group][gene].list_qr_FI:
|
|
474
|
+
if not (FI.hash in seq_id_list):
|
|
475
|
+
self.dict_dataset[group][gene].list_qr_FI.remove(FI)
|
|
476
|
+
|
|
477
|
+
for FI in self.dict_dataset[group][gene].list_og_FI:
|
|
478
|
+
if not (FI.hash in seq_id_list):
|
|
479
|
+
self.dict_dataset[group][gene].list_og_FI.remove(FI)
|
|
480
|
+
|
|
481
|
+
# Remove empty sequences
|
|
482
|
+
remove_hash = []
|
|
483
|
+
for seq in seq_list:
|
|
484
|
+
if len(str(seq.seq).replace("-", "")) == 0:
|
|
485
|
+
logging.debug(
|
|
486
|
+
f"{group} {gene} {seq.id} : {len(str(seq.seq).replace('-', ''))}"
|
|
487
|
+
)
|
|
488
|
+
remove_hash.append(seq.id)
|
|
489
|
+
|
|
490
|
+
remove_dict[group][gene] = remove_hash
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
for _hash in remove_hash:
|
|
494
|
+
if _hash in self.dict_dataset[group][gene].list_db_FI:
|
|
495
|
+
self.dict_dataset[group][gene].list_db_FI.pop(_hash)
|
|
496
|
+
if _hash in self.dict_dataset[group][gene].list_query_FI:
|
|
497
|
+
self.dict_dataset[group][gene].list_query_FI.pop(_hash)
|
|
498
|
+
if _hash in self.dict_dataset[group][gene].list_og_FI:
|
|
499
|
+
self.dict_dataset[group][gene].list_og_FI.pop(_hash)
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
# Remove unusable sequence and re-read it
|
|
503
|
+
## db_list, query_list, outgroup_list might has to be changed
|
|
504
|
+
seq_list = [
|
|
505
|
+
seq for seq in seq_list if not seq.id in remove_hash
|
|
506
|
+
]
|
|
507
|
+
SeqIO.write(
|
|
508
|
+
seq_list,
|
|
509
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
|
|
510
|
+
"fasta",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
## Transform alignment to vector
|
|
514
|
+
## Change gap to 0, and other characters to 1
|
|
515
|
+
vectors = [
|
|
516
|
+
np.fromiter(
|
|
517
|
+
re.sub(
|
|
518
|
+
r"[^0]", "1", re.sub(r"[\-]", "0", str(seq.seq))
|
|
519
|
+
),
|
|
520
|
+
dtype=np.int32,
|
|
521
|
+
)
|
|
522
|
+
for seq in seq_list
|
|
523
|
+
]
|
|
524
|
+
## Multiply vectors
|
|
525
|
+
vector_products = np.prod(np.vstack(vectors), axis=0)
|
|
526
|
+
logging.debug(
|
|
527
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta resulted multiplied vector {vector_products}"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
## If all value of vectors are zero, it means that all regions have at least one gap
|
|
531
|
+
## Raise warning for this
|
|
532
|
+
if np.all((vector_products == 0)):
|
|
533
|
+
logging.warning(
|
|
534
|
+
f"Alignment for {group} {gene} does not have any overlapping regions! Removing from analysis"
|
|
535
|
+
)
|
|
536
|
+
fail_list.append((group, gene))
|
|
537
|
+
# for tree, use hash dict with genus and species information
|
|
538
|
+
# Decoding process in done in tree building processes, so these alignments cannot be decoded. So decode them here
|
|
539
|
+
shutil.move(
|
|
540
|
+
f"{path.out_alignment}/{opt.runname}_MAFFT_{group}_{gene}.fasta",
|
|
541
|
+
f"{path.out_alignment}/hash/{opt.runname}_hash_MAFFT_{group}_{gene}.fasta",
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
hasher.decode(
|
|
545
|
+
tree_hash_dict,
|
|
546
|
+
f"{path.out_alignment}/hash/{opt.runname}_hash_MAFFT_{group}_{gene}.fasta",
|
|
547
|
+
f"{path.out_alignment}/{opt.runname}_MAFFT_{group}_{gene}.fasta",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Move failed alignment to failed directory
|
|
551
|
+
shutil.move(
|
|
552
|
+
f"{path.out_alignment}/{opt.runname}_MAFFT_{group}_{gene}.fasta",
|
|
553
|
+
f"{path.out_alignment}/failed/{opt.runname}_MAFFT_{group}_{gene}.fasta",
|
|
554
|
+
)
|
|
555
|
+
else:
|
|
556
|
+
logging.debug(
|
|
557
|
+
f"Alignment for {group} {gene} passed validation"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# If alignment corresponding to dataset does not exists, raise warning or error
|
|
561
|
+
pass
|
|
562
|
+
|
|
563
|
+
# Remove bad datasets
|
|
564
|
+
for fail in fail_list:
|
|
565
|
+
group = fail[0]
|
|
566
|
+
gene = fail[1]
|
|
567
|
+
self.dict_dataset[group].pop(gene)
|
|
568
|
+
logging.warning(f"Alignment for {group} {gene} has removed from analysis")
|
|
569
|
+
|
|
570
|
+
# Add issue
|
|
571
|
+
for fail in fail_list:
|
|
572
|
+
for FI in self.list_FI:
|
|
573
|
+
if FI.adjusted_group == fail[0]:
|
|
574
|
+
if FI.seq[fail[1]] != "":
|
|
575
|
+
FI.issues.add(f"alignfail:{fail[1]}")
|
|
576
|
+
|
|
577
|
+
print(fail_list)
|
|
578
|
+
|
|
579
|
+
logging.debug("Remove dict")
|
|
580
|
+
logging.debug(remove_dict)
|
|
581
|
+
|
|
582
|
+
# Remove removed sequences from dataset
|
|
583
|
+
for group in remove_dict:
|
|
584
|
+
for gene in remove_dict[group]:
|
|
585
|
+
if group in self.dict_dataset:
|
|
586
|
+
if gene in self.dict_dataset[group]:
|
|
587
|
+
for _hash in remove_dict[group][gene]:
|
|
588
|
+
if _hash in self.dict_dataset[group][gene].list_qr_FI:
|
|
589
|
+
self.dict_dataset[group][gene].list_qr_FI.remove(
|
|
590
|
+
self.dict_hash_FI[_hash]
|
|
591
|
+
)
|
|
592
|
+
logging.warning(
|
|
593
|
+
f"{self.dict_hash_ID[_hash]} removed from dataset {group} {gene}. Please check the alignment and see the region is correct"
|
|
594
|
+
)
|
|
595
|
+
if _hash in self.dict_dataset[group][gene].list_db_FI:
|
|
596
|
+
self.dict_dataset[group][gene].list_db_FI.remove(
|
|
597
|
+
self.dict_hash_FI[_hash]
|
|
598
|
+
)
|
|
599
|
+
logging.warning(
|
|
600
|
+
f"{self.dict_hash_ID[_hash]} removed from dataset {group} {gene}. Please check the alignment and see the region is correct"
|
|
601
|
+
)
|
|
602
|
+
if _hash in self.dict_dataset[group][gene].list_og_FI:
|
|
603
|
+
self.dict_dataset[group][gene].list_og_FI.remove(
|
|
604
|
+
self.dict_hash_FI[_hash]
|
|
605
|
+
)
|
|
606
|
+
logging.warning(
|
|
607
|
+
f"{self.dict_hash_ID[_hash]} removed from dataset {group} {gene}. Please check the alignment and see the region is correct"
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Finally, check again if the datasets meet criteria
|
|
611
|
+
final_fail_list = []
|
|
612
|
+
for group in self.dict_dataset:
|
|
613
|
+
for gene in self.dict_dataset[group]:
|
|
614
|
+
logging.debug(f"Validating alignment for {group} {gene}")
|
|
615
|
+
logging.debug(
|
|
616
|
+
f"list_qr_FI : {len(self.dict_dataset[group][gene].list_qr_FI)}"
|
|
617
|
+
)
|
|
618
|
+
logging.debug(
|
|
619
|
+
f"list_db_FI : {len(self.dict_dataset[group][gene].list_db_FI)}"
|
|
620
|
+
)
|
|
621
|
+
logging.debug(
|
|
622
|
+
f"list_og_FI : {len(self.dict_dataset[group][gene].list_og_FI)}"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if (
|
|
626
|
+
len(self.dict_dataset[group][gene].list_qr_FI)
|
|
627
|
+
+ len(self.dict_dataset[group][gene].list_db_FI)
|
|
628
|
+
+ len(self.dict_dataset[group][gene].list_og_FI)
|
|
629
|
+
< 4
|
|
630
|
+
):
|
|
631
|
+
logging.warning(
|
|
632
|
+
f"After removing invalid datasets from dataset {group} {gene}, the number of remaining sequences are under 4, removing from anlaysis."
|
|
633
|
+
)
|
|
634
|
+
final_fail_list.append((group, gene))
|
|
635
|
+
|
|
636
|
+
elif len(self.dict_dataset[group][gene].list_og_FI) < 1:
|
|
637
|
+
logging.warning(
|
|
638
|
+
f"After removing invalid datasets from dataset {group} {gene}, outgroup of the dataset has completely removed, removing from anlaysis."
|
|
639
|
+
)
|
|
640
|
+
final_fail_list.append((group, gene))
|
|
641
|
+
|
|
642
|
+
elif (
|
|
643
|
+
len(self.dict_dataset[group][gene].list_qr_FI)
|
|
644
|
+
+ len(self.dict_dataset[group][gene].list_db_FI)
|
|
645
|
+
< 1
|
|
646
|
+
):
|
|
647
|
+
logging.warning(
|
|
648
|
+
f"After removing invalid datasets from dataset {group} {gene}, dataset has completely removed, removing from anlaysis."
|
|
649
|
+
)
|
|
650
|
+
final_fail_list.append((group, gene))
|
|
651
|
+
|
|
652
|
+
# Remove bad datasets
|
|
653
|
+
for fail in final_fail_list:
|
|
654
|
+
group = fail[0]
|
|
655
|
+
gene = fail[1]
|
|
656
|
+
self.dict_dataset[group].pop(gene)
|
|
657
|
+
|
|
658
|
+
# If concatenated is the only left dataset, remove entire group
|
|
659
|
+
group_pop_list = []
|
|
660
|
+
for group in self.dict_dataset:
|
|
661
|
+
if len(self.dict_dataset[group].keys()) == 0:
|
|
662
|
+
group_pop_list.append(group)
|
|
663
|
+
elif (
|
|
664
|
+
len(self.dict_dataset[group].keys()) == 1
|
|
665
|
+
and "concatenated" in self.dict_dataset[group].keys()
|
|
666
|
+
):
|
|
667
|
+
group_pop_list.append(group)
|
|
668
|
+
|
|
669
|
+
for group in group_pop_list:
|
|
670
|
+
self.dict_dataset.pop(group)
|
|
671
|
+
logging.warning(
|
|
672
|
+
f"No alignment left for group {group}, removed from analysis"
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# Add issue: the number of sequences are insufficient
|
|
676
|
+
for fail in final_fail_list:
|
|
677
|
+
for FI in V.list_FI:
|
|
678
|
+
if FI.adjusted_group == fail[0]:
|
|
679
|
+
if FI.seq[fail[1]] != "":
|
|
680
|
+
FI.issues.add(f"lackseq")
|
|
681
|
+
|
|
682
|
+
# return V
|
|
683
|
+
|
|
684
|
+
# check inconsistency exists along identification result of each genes
|
|
685
|
+
def check_inconsistent(self):
|
|
686
|
+
set_gene = set(self.list_db_gene + self.list_qr_gene)
|
|
687
|
+
|
|
688
|
+
for _hash in self.dict_hash_FI:
|
|
689
|
+
FI = self.dict_hash_FI[_hash]
|
|
690
|
+
|
|
691
|
+
# Collect result from only used sequences
|
|
692
|
+
if FI.adjusted_group in self.dict_dataset:
|
|
693
|
+
# If any of appropriate gene used
|
|
694
|
+
if any(
|
|
695
|
+
key in self.dict_dataset[FI.adjusted_group] for key in FI.seq.keys()
|
|
696
|
+
):
|
|
697
|
+
inconsistent_flag = 0
|
|
698
|
+
|
|
699
|
+
for gene in set_gene:
|
|
700
|
+
# Check if data analysis had performed for specific FI, group, gene combination
|
|
701
|
+
if (
|
|
702
|
+
gene in FI.bygene_species
|
|
703
|
+
and len(FI.seq[gene]) > 0
|
|
704
|
+
and gene in self.dict_dataset[FI.adjusted_group]
|
|
705
|
+
):
|
|
706
|
+
# Check inconsistent identification across genes
|
|
707
|
+
if not (
|
|
708
|
+
any(
|
|
709
|
+
_sp in FI.final_species
|
|
710
|
+
for _sp in FI.bygene_species[gene].split("/")
|
|
711
|
+
)
|
|
712
|
+
):
|
|
713
|
+
inconsistent_flag = 1
|
|
714
|
+
|
|
715
|
+
if inconsistent_flag == 1:
|
|
716
|
+
FI.issues.add("inconsistent")
|