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.
@@ -0,0 +1,679 @@
1
+ # Performing multiple tree interpretation
2
+ from ete3 import Tree
3
+ from funvip.src import tree_interpretation
4
+ from funvip.src.tool import initialize_path, get_genus_species
5
+ from funvip.src.hasher import encode, decode
6
+ from funvip.src.reporter import Singlereport
7
+ from copy import deepcopy
8
+ import pandas as pd
9
+ import re
10
+ import sys, os
11
+ import shutil
12
+ import logging
13
+ import multiprocessing as mp
14
+
15
+
16
+ ### For single dataset
17
+ # Input : out, group, gene, V, path, opt
18
+ def pipe_module_tree_interpretation(
19
+ out,
20
+ group,
21
+ gene,
22
+ V,
23
+ path,
24
+ opt,
25
+ ):
26
+ # To reduce memory usage in multithreaded performance, copy necessary objects and then remove V
27
+ funinfo_dict = V.dict_hash_FI
28
+ funinfo_list = V.list_FI
29
+ hash_dict = V.dict_hash_name
30
+ query_list = V.dict_dataset[group][gene].list_qr_FI
31
+ outgroup = V.dict_dataset[group][gene].list_og_FI
32
+ partition = V.partition[group]
33
+
34
+ # for unexpectively included sequence during clustering
35
+ db_list = list(
36
+ set([FI for FI in V.list_FI if FI.datatype == "db"])
37
+ - set(outgroup)
38
+ - set(query_list)
39
+ )
40
+ genus_list = V.tup_genus
41
+
42
+ del V
43
+
44
+ # For get_genus_species
45
+ initialize_path(path)
46
+
47
+ # Tree name selection for tree construction software
48
+ tree_name = f"{path.out_tree}/hash/hash_{opt.runname}_{group}_{gene}.nwk"
49
+ logging.debug(f"{tree_name} entered tree interpretation")
50
+
51
+ # Check validity of file while importing
52
+ if os.path.isfile(tree_name):
53
+ try:
54
+ # If iqtree, missing supports are not shown
55
+ if opt.method.tree.lower() == "iqtree":
56
+ Tree(tree_name, format=0)
57
+ else:
58
+ Tree(tree_name, format=2)
59
+ except:
60
+ logging.error(f"[DEVELOPMENTAL ERROR] Failed on importing tree {tree_name}")
61
+ raise Exception
62
+ else:
63
+ logging.error(f"Cannot find {tree_name}")
64
+ raise Exception
65
+
66
+ # initialize before analysis
67
+ Tree_style = tree_interpretation.Tree_style()
68
+
69
+ # Read tree
70
+ tree_info = tree_interpretation.Tree_information(
71
+ tree_name, Tree_style, group, gene, opt
72
+ )
73
+
74
+ # Give necessary variables parsed from dataset
75
+ tree_info.db_list = db_list
76
+ tree_info.query_list = query_list
77
+ tree_info.outgroup = outgroup
78
+ tree_info.funinfo_dict = funinfo_dict
79
+
80
+ # Main phase
81
+ # calculate zero distance with alignment
82
+ if gene == "concatenated":
83
+ tree_info.calculate_zero(
84
+ alignment_file=f"{path.out_alignment}/hash/{opt.runname}_hash_trimmed_{group}_{gene}.fasta",
85
+ gene=gene,
86
+ partition_dict=partition,
87
+ )
88
+ else:
89
+ tree_info.calculate_zero(
90
+ alignment_file=f"{path.out_alignment}/hash/{opt.runname}_hash_trimmed_{group}_{gene}.fasta",
91
+ gene=gene,
92
+ partition_dict=None,
93
+ )
94
+
95
+ # Reroot outgroup and save original tree into image
96
+ tree_info.reroot_outgroup(
97
+ f"{path.out_tree}/hash_{opt.runname}_{group}_{gene}_original.svg"
98
+ )
99
+ # Decode hash of image
100
+ # Should work more on non-safe characters
101
+ tree_hash_dict = encode(funinfo_list, newick=True)
102
+
103
+ decode(
104
+ tree_hash_dict,
105
+ f"{path.out_tree}/hash_{opt.runname}_{group}_{gene}_original.svg",
106
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}_original.svg",
107
+ newick=True,
108
+ )
109
+
110
+ # In validation mode, use original sp. number
111
+ if opt.mode == "validation":
112
+ tree_info.reserve_sp()
113
+
114
+ # Reconstruct flat branches if option given
115
+ if opt.solveflat is True:
116
+ tree_info.t = tree_info.reconstruct(
117
+ clade=tree_info.t.copy("newick"), gene=gene, opt=opt
118
+ )
119
+
120
+ # reorder tree for pretty look
121
+ tree_info.t.ladderize(direction=1)
122
+
123
+ # save current status into save version of tree
124
+ tree_info.t_publish = deepcopy(tree_info.t)
125
+
126
+ # Search tree and delimitate species
127
+ tree_info.tree_search(tree_info.t, gene)
128
+
129
+ # Move original newick and replace with adjusted ones
130
+ shutil.move(
131
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}.nwk",
132
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}_original.nwk",
133
+ )
134
+ tree_info.t.write(
135
+ format=0, outfile=f"{path.out_tree}/{opt.runname}_{group}_{gene}.nwk"
136
+ )
137
+ decode(
138
+ tree_hash_dict,
139
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}.nwk",
140
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}.nwk",
141
+ newick=True,
142
+ )
143
+
144
+ return tree_info
145
+
146
+
147
+ ### synchronize sp. numbers from multiple dataset
148
+ # to use continuous sp numbers over trees
149
+ # Seperated from multithreading, because this step should traverse over multiple trees, therefore cannot be done simultaniously
150
+ def synchronize(V, path, tree_info_list):
151
+ # Gets hash dict, and returns taxon name of hash_dict
152
+ # Generate final taxon name for synchronizing
153
+ def get_new_taxon(hash_list, hash_taxon_dict):
154
+ # Get candidate taxons from hash_taxon_dict
155
+ taxon_candidates = set()
156
+ for _hash in hash_list:
157
+ if _hash in hash_taxon_dict:
158
+ taxon_candidates.add(hash_taxon_dict[_hash])
159
+ else:
160
+ logging.debug(
161
+ f"{_hash} does not seems to be analyzed from concatenated dataset"
162
+ )
163
+
164
+ list_taxon_candidates = sorted(list(taxon_candidates))
165
+
166
+ # Merge genus
167
+ genus = "/".join(sorted(list(set(t[0][0] for t in list_taxon_candidates))))
168
+
169
+ # Merge species
170
+ species_list = []
171
+
172
+ clade_cnt_set = set()
173
+ for t in list_taxon_candidates:
174
+ # If the taxon is unique
175
+ if not (t[0], 2) in hash_taxon_dict.values():
176
+ species_list.append(t[0][1])
177
+ else:
178
+ species_list.append(f"{t[0][1]} {t[1]}")
179
+ clade_cnt_set.add(t[1])
180
+
181
+ # Work with species with numbers
182
+ dict_species = {}
183
+ for s in species_list:
184
+ splited_species = s.split(" ")
185
+ try:
186
+ # Collect with numbers
187
+ int(splited_species[-1])
188
+ if not (" ".join(splited_species[:-1]) in dict_species):
189
+ dict_species[" ".join(splited_species[:-1])] = [
190
+ int(splited_species[-1])
191
+ ]
192
+ else:
193
+ dict_species[" ".join(splited_species[:-1])].append(
194
+ int(splited_species[-1])
195
+ )
196
+ except:
197
+ dict_species[s] = [0]
198
+
199
+ species = ""
200
+
201
+ for key in sorted(list(dict_species.keys())):
202
+ if len(set(dict_species[key]) - set([0])) == 0:
203
+ species += key
204
+ species += "/"
205
+ else:
206
+ species_numbers = [
207
+ str(x) for x in sorted(list(set(dict_species[key]) - set([0])))
208
+ ]
209
+ species += key
210
+ species += " "
211
+ species += "/".join(species_numbers)
212
+
213
+ # Remove last slash
214
+ if species.endswith("/"):
215
+ species = species[:-1]
216
+
217
+ if len(clade_cnt_set) == 1:
218
+ clade_cnt = list(clade_cnt_set)[0]
219
+ else:
220
+ clade_cnt = 0
221
+
222
+ return (genus, species), clade_cnt
223
+ ### End of get_new_taxon
224
+
225
+ ## Initialize
226
+ # get available groups per genus
227
+ tree_info_dict = {}
228
+ # hash : corresponding taxon
229
+ hash_taxon_dict = {}
230
+ # genus : cnt, counting sp. numbers
231
+ sp_cnt_dict = {}
232
+
233
+ # To synchronize sp. number by genus, generate by-group dataset
234
+ for tree_info in tree_info_list:
235
+ if not (tree_info.group in tree_info_dict):
236
+ tree_info_dict[tree_info.group] = {tree_info.gene: tree_info}
237
+ elif not (tree_info.gene in tree_info_dict[tree_info.group]):
238
+ tree_info_dict[tree_info.group][tree_info.gene] = tree_info
239
+ else:
240
+ logging.error("DEVELOPMENTAL ERROR, DUPLICATED TREE_INFO")
241
+ raise Exception
242
+
243
+ # Memoize iterative calling
244
+ # For each group list
245
+ valid_hash_dict = {}
246
+ # DB
247
+ for group in tree_info_dict:
248
+ valid_hash_dict[group] = [
249
+ _hash
250
+ for _hash in V.dict_hash_FI
251
+ if V.dict_hash_FI[_hash].datatype == "db"
252
+ and V.dict_hash_FI[_hash].adjusted_group == group
253
+ ]
254
+ # Query
255
+ query_hash_list = [
256
+ _hash for _hash in V.dict_hash_FI if V.dict_hash_FI[_hash].datatype == "query"
257
+ ]
258
+
259
+ ## Starting with concatenated
260
+ # In priority, count corresponding group taxa first
261
+ for group in tree_info_dict:
262
+ if "concatenated" in tree_info_dict[group]:
263
+ # Catch concatenated tree
264
+ tree_info = tree_info_dict[group]["concatenated"]
265
+ # Get list of hash in interest
266
+ valid_hash_list = valid_hash_dict[group]
267
+ for taxon in tree_info.collapse_dict:
268
+ # Get all monophyletic clades from tree and make it list
269
+ clade_list = tree_info.collapse_dict[taxon]
270
+ for n, clade in enumerate(clade_list):
271
+ # list of hash in clade
272
+ hash_list = [leaf[0] for leaf in clade.leaf_list]
273
+ # If any of the leaf consisting clade is included to valid_hash_list
274
+ if any(_h in valid_hash_list for _h in hash_list):
275
+ for _hash in hash_list:
276
+ if not (_hash) in hash_taxon_dict:
277
+ if len(clade_list) == 1:
278
+ hash_taxon_dict[_hash] = (taxon, 0)
279
+ else:
280
+ hash_taxon_dict[_hash] = (taxon, n + 1)
281
+ elif _hash in hash_taxon_dict and hash_taxon_dict[
282
+ _hash
283
+ ] != (taxon, n):
284
+ logging.debug(
285
+ f"{_hash} collided while putting in hash_taxon_dict"
286
+ )
287
+
288
+ # If leaf consisting with only queries, that won't collide with other groups
289
+ # This is about new species clade
290
+ if all(_h in query_hash_list for _h in hash_list):
291
+ # If this clade is first sp. species for the genus, start counting sp. number
292
+ if not (taxon[0] in sp_cnt_dict):
293
+ sp_cnt_dict[taxon[0]] = 1
294
+ for _hash in hash_list:
295
+ if not (_hash) in hash_taxon_dict:
296
+ hash_taxon_dict[_hash] = (
297
+ (
298
+ taxon[0],
299
+ f"sp. {sp_cnt_dict[taxon[0]]}",
300
+ ),
301
+ 0,
302
+ )
303
+
304
+ elif (
305
+ _hash in hash_taxon_dict
306
+ and hash_taxon_dict[_hash] != taxon
307
+ ):
308
+ logging.debug(
309
+ f"{_hash} collided while putting in hash_taxon_dict"
310
+ )
311
+
312
+ sp_cnt_dict[taxon[0]] += 1
313
+
314
+ # Next, taxa that doesn't belongs to any of the group
315
+ # concatenated first
316
+ for group in tree_info_dict:
317
+ if "concatenated" in tree_info_dict[group]:
318
+ tree_info = tree_info_dict[group]["concatenated"]
319
+
320
+ all_hash = [
321
+ _hash
322
+ for _hash in V.dict_hash_FI
323
+ if V.dict_hash_FI[_hash].datatype == "db"
324
+ and V.dict_hash_FI[_hash].adjusted_group != group
325
+ ]
326
+
327
+ # Get list of hash not in interest
328
+ invalid_hash_list = list(set(all_hash) - set(valid_hash_dict[group]))
329
+ for taxon in tree_info.collapse_dict:
330
+ clade_list = tree_info.collapse_dict[taxon]
331
+ for n, clade in enumerate(clade_list):
332
+ hash_list = [leaf[0] for leaf in clade.leaf_list]
333
+ # If the hash has not been counted in any of the tree,
334
+ if not (any(_h in invalid_hash_list for _h in hash_list)):
335
+ for _hash in hash_list:
336
+ if not (_hash in hash_taxon_dict):
337
+ logging.debug(
338
+ f"New hash: {_hash} {_hash.adjusted_group} from {group}"
339
+ )
340
+ hash_taxon_dict[_hash] = (taxon, n)
341
+ elif _hash in hash_taxon_dict and hash_taxon_dict[
342
+ _hash
343
+ ] != (taxon, n):
344
+ logging.debug(
345
+ f"{_hash} collided while putting in hash_taxon_dict. Tried to put {(taxon, n+1)}, but existing {hash_taxon_dict[_hash]}"
346
+ )
347
+
348
+ # Then, non-concatenated
349
+ for group in tree_info_dict:
350
+ for gene in tree_info_dict[group]:
351
+ if gene != "concatenated":
352
+ tree_info = tree_info_dict[group]["concatenated"]
353
+ # Get list of hash in interest
354
+ valid_hash_list = valid_hash_dict[group]
355
+ for taxon in tree_info.collapse_dict:
356
+ clade_list = tree_info.collapse_dict[taxon]
357
+ for n, clade in enumerate(clade_list):
358
+ hash_list = [leaf[0] for leaf in clade.leaf_list]
359
+ # If the hash has not been counted in any of the tree,
360
+ if not (any(_h in valid_hash_list for _h in hash_list)):
361
+ for _hash in hash_list:
362
+ if not (_hash in hash_taxon_dict):
363
+ hash_taxon_dict[_hash] = (taxon, n + 1)
364
+ elif _hash in hash_taxon_dict and hash_taxon_dict[
365
+ _hash
366
+ ] != (taxon, n + 1):
367
+ logging.debug(
368
+ f"{_hash} collided while putting in hash_taxon_dict. Tried to put {(taxon, n+1)}, but existing {hash_taxon_dict[_hash]}"
369
+ )
370
+
371
+ # print(group, _hash, taxon, n + 1)
372
+
373
+ """
374
+
375
+
376
+ # Now update from concatenated
377
+ # Remove original taxon, and add by clade taxon
378
+ for group in tree_info_dict:
379
+ for gene in tree_info_dict[group]:
380
+ if gene == "concatenated":
381
+ tree_info = tree_info_dict[group][gene]
382
+ # Before taxon, after taxon update list
383
+ remove_list = [] # [taxon1, taxon2, taxon3 ...]
384
+ add_list = {} # [taxon1 : [clade1], taxon2 : [clade2] ...]
385
+ for taxon in tree_info.collapse_dict:
386
+ clade_list = tree_info.collapse_dict[taxon]
387
+ remove_list.append(taxon)
388
+ for clade in clade_list:
389
+ hash_list = [leaf[0] for leaf in clade.leaf_list]
390
+ new_taxon, clade_cnt = get_new_taxon(hash_list, hash_taxon_dict)
391
+ clade.taxon = new_taxon
392
+ clade.clade_cnt = clade_cnt
393
+ if not (new_taxon in add_list):
394
+ add_list[new_taxon] = [clade]
395
+ else:
396
+ add_list[new_taxon].append(clade)
397
+
398
+ # Remove previous taxon
399
+ for taxon in remove_list:
400
+ tree_info.collapse_dict.pop(taxon)
401
+
402
+ # Add synchronized taxon
403
+ for taxon in add_list:
404
+ tree_info.collapse_dict[taxon] = add_list[taxon]
405
+ else:
406
+ tree_info = tree_info_dict[group][gene]
407
+ # Before taxon, after taxon update list
408
+ remove_list = [] # [taxon1, taxon2, taxon3 ...]
409
+ add_list = {} # [taxon1 : [clade1], taxon2 : [clade2] ...]
410
+ for taxon in tree_info.collapse_dict:
411
+ clade_list = tree_info.collapse_dict[taxon]
412
+ remove_list.append(taxon)
413
+ for clade in clade_list:
414
+ hash_list = [leaf[0] for leaf in clade.leaf_list]
415
+ new_taxon, clade_cnt = get_new_taxon(hash_list, hash_taxon_dict)
416
+ clade.taxon = new_taxon
417
+ clade.clade_cnt = clade_cnt
418
+ if not (new_taxon in add_list):
419
+ add_list[new_taxon] = [clade]
420
+ else:
421
+ add_list[new_taxon].append(clade)
422
+
423
+ # Remove previous taxon
424
+ for taxon in remove_list:
425
+ tree_info.collapse_dict.pop(taxon)
426
+
427
+ # Add synchronized taxon
428
+ for taxon in add_list:
429
+ tree_info.collapse_dict[taxon] = add_list[taxon]
430
+
431
+
432
+ """
433
+
434
+ # raise Exception
435
+
436
+ # Return sp number fixed tree_info_list
437
+ return tree_info_list
438
+
439
+
440
+ ### Visualization after synchronization
441
+ def pipe_module_tree_visualization(
442
+ tree_info,
443
+ V,
444
+ path,
445
+ opt,
446
+ ):
447
+ ######### Fix collapse_dict.keys()
448
+
449
+ group = tree_info.group
450
+ gene = tree_info.gene
451
+ genus_list = list(V.tup_genus)
452
+ genus_list.append("AMBIGUOUSGENUS")
453
+ genus_list = tuple(genus_list)
454
+
455
+ # Collapse tree branches for visualization
456
+ taxon_string_dict = tree_info.collapse_tree()
457
+
458
+ # print(f"taxon_string_list | {group} {gene}:\n {taxon_string_list}\n")
459
+
460
+ # Polish tree image
461
+ tree_info.polish_image(
462
+ f"{path.out_tree}/{opt.runname}_{group}_{gene}.svg",
463
+ taxon_string_dict,
464
+ genus_list,
465
+ )
466
+
467
+ # sort taxon order
468
+ list_taxon_1 = [
469
+ taxon
470
+ for taxon in tree_info.collapse_dict.keys()
471
+ if not (taxon[1].startswith("sp."))
472
+ ]
473
+
474
+ list_taxon_2 = [
475
+ taxon for taxon in tree_info.collapse_dict.keys() if taxon[1].startswith("sp.")
476
+ ]
477
+ list_taxon_1.sort(key=lambda x: x[1])
478
+ list_taxon_2.sort(key=lambda x: x[1])
479
+ list_taxon = list_taxon_1 + list_taxon_2
480
+
481
+ # Declare report collection
482
+ report_list = []
483
+ for taxon in list_taxon:
484
+ # If only one taxon exists, enumerate does not work properly
485
+ if len(tree_info.collapse_dict[taxon]) <= 1:
486
+ collapse_info = tree_info.collapse_dict[taxon][0]
487
+ # Get each of the leaf result to report
488
+ for leaf in collapse_info.leaf_list:
489
+ report = Singlereport()
490
+ report.id = V.dict_hash_FI[leaf[0]].original_id
491
+ report.hash = V.dict_hash_FI[leaf[0]].hash
492
+ report.update_group(V.dict_hash_FI[leaf[0]].adjusted_group)
493
+ report.update_group_analysis(group)
494
+ report.update_gene(gene)
495
+ report.update_species_original(
496
+ get_genus_species(leaf[2], genus_list=genus_list)
497
+ )
498
+ # joining genus and species
499
+ report.update_species_assigned(" ".join(taxon))
500
+ # report.update_species_assigned(taxon[1])
501
+ report.ambiguous = collapse_info.clade_cnt
502
+ report.flat = collapse_info.flat
503
+
504
+ report_list.append(report)
505
+
506
+ # If more than one taxon exists,
507
+ else:
508
+ for n, collapse_info in enumerate(tree_info.collapse_dict[taxon]):
509
+ for leaf in collapse_info.leaf_list:
510
+ report = Singlereport()
511
+ report.id = V.dict_hash_FI[leaf[0]].original_id
512
+ report.hash = V.dict_hash_FI[leaf[0]].hash
513
+ report.update_group(V.dict_hash_FI[leaf[0]].adjusted_group)
514
+ report.update_group_analysis(group)
515
+ report.update_gene(gene)
516
+ report.update_species_original(
517
+ get_genus_species(leaf[2], genus_list=genus_list)
518
+ )
519
+ report.update_species_assigned((f"{taxon[0]} {taxon[1]} {n+1}"))
520
+
521
+ report.ambiguous = collapse_info.clade_cnt
522
+ report.flat = collapse_info.flat
523
+
524
+ report_list.append(report)
525
+
526
+ return report_list
527
+
528
+
529
+ ### For all datasets, multiprocessing part
530
+ def pipe_tree_interpretation(V, path, opt):
531
+ # Generate tree_interpretation opt to run
532
+ tree_interpretation_opt = []
533
+
534
+ # Reset bygene_species for rerun
535
+ for key in V.dict_hash_FI: # funinfo_dict
536
+ for gene in V.dict_hash_FI[key].bygene_species:
537
+ V.dict_hash_FI[key].bygene_species[gene] = V.dict_hash_FI[key].ori_species
538
+
539
+ for group in V.dict_dataset:
540
+ for gene in V.dict_dataset[group]:
541
+ logging.debug(f"pipe_tree_interpretation {group} {gene}")
542
+ # Condition 1 : draw all trees
543
+ cond1 = opt.queryonly is False
544
+ # Condition 2 : When query included
545
+ cond2 = len(V.dict_dataset[group][gene].list_qr_FI) > 0
546
+ # Condition 3 : When any of the branches of the tree is valid in concatenated analysis
547
+ cond3 = (len(V.dict_dataset[group]["concatenated"].list_qr_FI) > 0) and any(
548
+ FI.hash
549
+ in [
550
+ x.hash
551
+ for x in V.dict_dataset[group][gene].list_qr_FI
552
+ + V.dict_dataset[group][gene].list_db_FI
553
+ + V.dict_dataset[group][gene].list_og_FI
554
+ ]
555
+ for FI in V.dict_dataset[group]["concatenated"].list_qr_FI
556
+ + V.dict_dataset[group]["concatenated"].list_db_FI
557
+ + V.dict_dataset[group]["concatenated"].list_og_FI
558
+ )
559
+
560
+ # Interpret tree when valid condition
561
+ if cond1 or cond2 or cond3:
562
+ if len(V.dict_dataset[group][gene].list_og_FI) > 0:
563
+ # Generating tree_interpretation opts for multithreading support
564
+ tree_interpretation_opt.append(
565
+ (
566
+ f"{opt.runname}_{group}_{gene}",
567
+ group,
568
+ gene,
569
+ V,
570
+ path,
571
+ opt,
572
+ )
573
+ )
574
+ # However, if outgroup does not exists, warn it
575
+ else:
576
+ logging.warning(
577
+ f"Failed interpreting tree {group} {gene} because no outgroup available"
578
+ )
579
+
580
+ ## Tree interpretation - outgroup, reconstruction(solve_flat), collapsing
581
+ if opt.verbose < 3:
582
+ p = mp.Pool(opt.thread)
583
+ tree_info_list = p.starmap(
584
+ pipe_module_tree_interpretation, tree_interpretation_opt
585
+ )
586
+ p.close()
587
+ p.join()
588
+
589
+ else:
590
+ # non-multithreading mode for debugging
591
+ tree_info_list = [
592
+ pipe_module_tree_interpretation(*option)
593
+ for option in tree_interpretation_opt
594
+ ]
595
+
596
+ # Gather flat branch issues
597
+ for tree_info in tree_info_list:
598
+ for flat_hash in tree_info.flat_clades:
599
+ FI = V.dict_hash_FI[flat_hash]
600
+ FI.issues.add(f"flat:{tree_info.gene}")
601
+
602
+ synchronized_tree_info_list = synchronize(V, path, tree_info_list)
603
+ tree_info_list = synchronized_tree_info_list
604
+ # Generate visualization option to run
605
+ tree_visualization_opt = []
606
+ for tree_info in tree_info_list:
607
+ tree_visualization_opt.append((tree_info, V, path, opt))
608
+
609
+ ## Tree visualization
610
+ if opt.verbose < 3:
611
+ p = mp.Pool(opt.thread)
612
+ tree_visualization_result = p.starmap(
613
+ pipe_module_tree_visualization, tree_visualization_opt
614
+ )
615
+ p.close()
616
+ p.join()
617
+
618
+ else:
619
+ # non-multithreading mode for debugging
620
+ tree_visualization_result = [
621
+ pipe_module_tree_visualization(*option) for option in tree_visualization_opt
622
+ ]
623
+
624
+ ### Collect identifiation result to V for reporting
625
+ # Merge report list
626
+ all_report_list = [
627
+ x for report_list in tree_visualization_result for x in report_list
628
+ ]
629
+
630
+ # hash_dict_analysis to prevent overwrite analysis from other tree
631
+ # hash : group_analysis
632
+ hash_dict_analysis = {}
633
+
634
+ # Add final identification result
635
+ # Concatenated first
636
+ for singlereport in all_report_list:
637
+ FI = V.dict_hash_FI[singlereport.hash]
638
+ # Concatenated
639
+ if singlereport.gene == "concatenated":
640
+ cond = 0
641
+ # If the strain has not been reported
642
+ if not singlereport.hash in hash_dict_analysis:
643
+ cond = 1
644
+ # If the strain has reported, but not in major tree
645
+ else:
646
+ if hash_dict_analysis[singlereport.hash] != singlereport.group:
647
+ cond = 2
648
+
649
+ if cond > 0:
650
+ FI.final_species = singlereport.species_assigned
651
+ FI.species_identifier = singlereport.ambiguous
652
+
653
+ if singlereport.ambiguous > 0:
654
+ FI.issues.add("polyphyly:concatenated")
655
+
656
+ if singlereport.flat is True:
657
+ FI.flat.append("concatenated")
658
+
659
+ hash_dict_analysis[singlereport.hash] = singlereport.group_analysis
660
+
661
+ # Non-concatenated
662
+ for singlereport in all_report_list:
663
+ FI = V.dict_hash_FI[singlereport.hash]
664
+ if singlereport.gene != "concatenated":
665
+ if singlereport.hash in hash_dict_analysis:
666
+ # In each gene tree, follow the group which taxon analyzed from concatenated tree
667
+ if hash_dict_analysis[singlereport.hash] == singlereport.group_analysis:
668
+ FI.bygene_species[singlereport.gene] = singlereport.species_assigned
669
+
670
+ if singlereport.ambiguous > 0:
671
+ FI.issues.add(f"polyphyly:{singlereport.gene}")
672
+
673
+ if singlereport.flat is True:
674
+ FI.flat.append(singlereport.gene)
675
+ else:
676
+ # If not found, FI would be additional database sequences outside the group added for ambiguities.
677
+ pass
678
+
679
+ return V, path, opt