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.
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")