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,1379 @@
1
+ # tree interpretation pipeline - collapse and visualize tree
2
+ from ete3 import (
3
+ Tree,
4
+ TreeStyle,
5
+ NodeStyle,
6
+ TextFace,
7
+ CircleFace,
8
+ RectFace,
9
+ faces,
10
+ )
11
+ from Bio import SeqIO
12
+ from copy import deepcopy
13
+ from time import sleep
14
+ import lxml.etree as ET
15
+ import pandas as pd
16
+ from functools import lru_cache
17
+ from funvip.src.tool import get_id, get_genus_species
18
+ import dendropy
19
+ import collections
20
+ import os
21
+ import re
22
+ import sys
23
+ import json
24
+
25
+ # Default zero length branch for concatenation
26
+ CONCAT_ZERO = 0 # for better binding
27
+
28
+ # For colored logging
29
+ bold_red = "\x1b[31;1m"
30
+ yellow = "\x1b[33;20m"
31
+ green = "\x1b[92m"
32
+ reset = "\x1b[0m"
33
+
34
+
35
+ ## Get maximum tree distance among all leaf pairs in given tree
36
+ def get_max_distance(tree):
37
+ max_distance = 0
38
+
39
+ # To prevent affecting tree
40
+ tree = deepcopy(tree)
41
+ (farthest_node, max_distance) = tree.detach().get_farthest_node()
42
+ return max_distance
43
+
44
+
45
+ ## divide string by new line character to prevent long string from being cut
46
+ ## by the maximum length of the string
47
+ def divide_by_max_len(string, max_len, sep=" "):
48
+ final_string = ""
49
+ tmp_string = ""
50
+
51
+ for char in string:
52
+ if char == sep:
53
+ if len(tmp_string) < max_len:
54
+ tmp_string += char
55
+ else:
56
+ tmp_string += char
57
+ final_string += tmp_string + "\n"
58
+ tmp_string = ""
59
+ else:
60
+ tmp_string += char
61
+ final_string += tmp_string
62
+ return final_string
63
+
64
+
65
+ # Per clade information
66
+ class Collapse_information:
67
+ def __init__(self):
68
+ self.query_list = []
69
+ self.db_list = []
70
+ self.outgroup = []
71
+ self.clade = None # partial tree clade
72
+ self.leaf_list = []
73
+ self.clade_cnt = 0 # if clade with same name exists, use this as counter
74
+ self.collapse_type = (
75
+ "" # line - for single clade / triangle - for multiple clade
76
+ )
77
+ self.color = "" # color after collapsed
78
+ self.height = ""
79
+ self.width = ""
80
+ self.taxon = "" # taxon name to be shown
81
+ self.n_db = 0
82
+ self.n_query = 0
83
+ self.n_others = 0
84
+ self.flat = False
85
+
86
+ def __str__(self):
87
+ return f"clade {self.taxon} with {len(self.leaf_list)} leaves"
88
+
89
+ def __repr__(self):
90
+ return f"clade {self.taxon} with {len(self.leaf_list)} leaves"
91
+
92
+
93
+ ## concat two given clade object and return
94
+ def concat_clade(
95
+ clade1,
96
+ clade2,
97
+ dist1=CONCAT_ZERO,
98
+ dist2=CONCAT_ZERO,
99
+ support1=1,
100
+ support2=1,
101
+ root_dist=CONCAT_ZERO,
102
+ root_support=0,
103
+ ):
104
+ tmp = Tree()
105
+ tmp.dist = root_dist
106
+ tmp.support = root_support
107
+ tmp.add_child(clade1, dist=dist1, support=support1)
108
+ tmp.add_child(clade2, dist=dist2, support=support2)
109
+ return tmp
110
+
111
+
112
+ ## concat all given branches for concatenation
113
+ # clades were given in tuble, and root_dist is given
114
+ def concat_all(clade_tuple, root_dist, root_support=0):
115
+ if len(clade_tuple) == 0:
116
+ print("No clade input found, abort")
117
+ raise Exception
118
+ # If one clade were input, return self
119
+ elif len(clade_tuple) == 1:
120
+ return clade_tuple[0].copy("newick")
121
+ # If two clades were input, concat it and return
122
+ elif len(clade_tuple) == 2:
123
+ return_clade = clade_tuple[0].copy("newick")
124
+ return_clade = concat_clade(
125
+ clade1=return_clade,
126
+ clade2=clade_tuple[1].copy("newick"),
127
+ dist1=return_clade.dist,
128
+ dist2=clade_tuple[1].dist,
129
+ support1=return_clade.support,
130
+ support2=clade_tuple[1].support,
131
+ root_dist=root_dist,
132
+ root_support=root_support,
133
+ )
134
+ # If more than 3 clades were input, iteratively concat
135
+ # If more than 2 species exists, and sp included, which taxon sp should be included cannot be decided
136
+ # In that case, move sp clade to last
137
+ elif len(clade_tuple) >= 3:
138
+ return_clade = clade_tuple[0].copy("newick")
139
+ for c in clade_tuple[1:-1]:
140
+ return_clade = concat_clade(
141
+ clade1=return_clade,
142
+ clade2=c.copy("newick"),
143
+ dist1=return_clade.dist,
144
+ dist2=c.dist,
145
+ support1=return_clade.support,
146
+ support2=c.support,
147
+ )
148
+ return_clade = concat_clade(
149
+ clade1=return_clade,
150
+ clade2=clade_tuple[-1].copy("newick"),
151
+ dist1=return_clade.dist,
152
+ dist2=clade_tuple[-1].dist,
153
+ support1=return_clade.support,
154
+ support2=clade_tuple[-1].support,
155
+ root_dist=root_dist,
156
+ root_support=root_support,
157
+ )
158
+ else:
159
+ raise Exception
160
+
161
+ return return_clade
162
+
163
+
164
+ # Default tree style
165
+ class Tree_style:
166
+ def __init__(self):
167
+ self.ts = TreeStyle()
168
+ self.ts.scale = 1000
169
+ # self.ts.show_branch_length = True
170
+ # self.ts.show_branch_support = True
171
+ self.ts.branch_vertical_margin = 10
172
+ self.ts.allow_face_overlap = True
173
+ self.ts.children_faces_on_top = True
174
+ self.ts.complete_branch_lines_when_necessary = False
175
+ self.ts.extra_branch_line_color = "black"
176
+ self.ts.margin_left = 200
177
+ self.ts.margin_right = 200
178
+ self.ts.margin_top = 200
179
+ self.ts.margin_bottom = 200
180
+ self.ts.show_leaf_name = False
181
+
182
+
183
+ # Main tree information class
184
+ class Tree_information:
185
+ def __init__(self, tree, Tree_style, group, gene, opt):
186
+ self.tree_name = tree # for debugging
187
+ self.t = Tree(tree)
188
+ self.t_publish = (
189
+ None # for publish tree - will substitute tree_original in long_term
190
+ )
191
+ self.dendro_t = dendropy.Tree.get(
192
+ path=self.tree_name, schema="newick"
193
+ ) # dendropy format for distance calculation
194
+
195
+ # if support ranges from 0 to 1, change it from 0 to 100
196
+ # b for branch
197
+ support_set = set()
198
+ for b in self.t.traverse():
199
+ support_set.add(b.support)
200
+
201
+ if max(support_set) <= 1:
202
+ for b in self.t.traverse():
203
+ b.support = int(100 * b.support)
204
+
205
+ self.query_list = []
206
+ self.db_list = []
207
+ self.outgroup = []
208
+ self.outgroup_leaf_name_list = [] # hash list of outgroup
209
+ self.outgroup_group = [] # list of groups that outgroup sequences designated
210
+ self.funinfo_dict = {} # leaf.name (hash) : Funinfo
211
+
212
+ self.sp_cnt = 1
213
+ self.reserved_sp = set()
214
+
215
+ self.Tree_style = Tree_style
216
+ self.group = group
217
+ self.gene = gene
218
+ self.opt = opt
219
+
220
+ self.collapse_dict = {} # { taxon name : collapse_info }
221
+
222
+ self.outgroup_clade = None
223
+ self.bgstate = 1
224
+ self.additional_clustering = True
225
+ self.zero = 0.00000100000050002909
226
+
227
+ self.flat_clades = []
228
+
229
+ # to find out already existing new species number to avoid overlapping
230
+ # e.g. avoid sp 5 if P. sp 5 already exsits in database
231
+ def reserve_sp(self):
232
+ for leaf in self.t.iter_leaves():
233
+ sys.stdout.flush()
234
+ taxon = (
235
+ self.funinfo_dict[leaf.name].genus,
236
+ self.funinfo_dict[leaf.name].ori_species,
237
+ )
238
+ sys.stdout.flush()
239
+ if taxon[1].split(" ")[0] in ("sp", "sp."):
240
+ self.reserved_sp.add(" ".join(taxon[1].split(" ")[1:]))
241
+
242
+ # this function decides whether the string is db or query
243
+ @lru_cache(maxsize=10000)
244
+ def decide_type(self, string, by="hash", priority="query"):
245
+ query = False
246
+ db = False
247
+
248
+ query_list = [FI.hash for FI in self.query_list]
249
+ db_list = [FI.hash for FI in self.db_list]
250
+ outgroup_list = [FI.hash for FI in self.outgroup]
251
+
252
+ if by == "hash":
253
+ if string in query_list:
254
+ return "query"
255
+ elif string in db_list:
256
+ return "db"
257
+ elif string in outgroup_list:
258
+ return "outgroup"
259
+ else:
260
+ return "none"
261
+
262
+ else:
263
+ print(
264
+ f"{bold_red}[ERROR] DEVELOPMENTAL ERROR, UNEXPECTED by for decide_type{reset}"
265
+ )
266
+ raise Exception
267
+
268
+ # Calculate zero length branch length cutoff with given tree and alignment
269
+ def calculate_zero(self, alignment_file, gene, partition_dict):
270
+ # Parse alignment
271
+ seq_list = list(SeqIO.parse(alignment_file, "fasta"))
272
+
273
+ # Check if tree leaves and alignments are consensus
274
+ hash_list_tree = [leaf.name for leaf in self.t]
275
+ hash_list_alignment = [seq.id for seq in seq_list]
276
+
277
+ if collections.Counter(hash_list_tree) != collections.Counter(
278
+ hash_list_alignment
279
+ ):
280
+ print(
281
+ f"{bold_red}[ERROR] content of tree and alignment is not identical for {self.tree_name}{reset}"
282
+ )
283
+ raise Exception
284
+
285
+ # Find identical or including pairs in alignment
286
+ identical_pairs = []
287
+ different_pairs = []
288
+ for seq1 in seq_list:
289
+ for seq2 in seq_list:
290
+ if not (
291
+ str(seq1.id).strip() == str(seq2.id).strip()
292
+ or (seq1.id, seq2.id) in identical_pairs
293
+ or (seq2.id, seq1.id) in identical_pairs
294
+ ):
295
+ # Chenge unusable chars into gap
296
+ seq1_str = str(seq1.seq).lower()
297
+ seq2_str = str(seq2.seq).lower()
298
+
299
+ for char in set(seq1_str) - {"a", "t", "g", "c", "-"}:
300
+ seq1_str = seq1_str.replace(char, "-")
301
+
302
+ for char in set(seq2_str) - {"a", "t", "g", "c", "-"}:
303
+ seq2_str = seq2_str.replace(char, "-")
304
+
305
+ identical_flag = True
306
+ # To prevent distance among different region detected as zero in concatenated analysis
307
+ overlapping_cnt = 0
308
+
309
+ if gene == "concatenated":
310
+ len_dict = partition_dict["len"]
311
+ gene_order = partition_dict["order"]
312
+
313
+ valid_index = []
314
+
315
+ # calculate valid index to check
316
+ previous_index = 0
317
+
318
+ for gene in gene_order:
319
+ start = previous_index
320
+ end = previous_index + len_dict[gene] - 1
321
+
322
+ for n in range(
323
+ previous_index, len_dict[gene] + previous_index
324
+ ):
325
+ if seq1_str[n] != "-" and seq2_str[n] != "-":
326
+ start = n
327
+ break
328
+
329
+ for n in range(
330
+ len_dict[gene] + previous_index - 1,
331
+ previous_index - 1,
332
+ -1,
333
+ ):
334
+ if seq1_str[n] != "-" and seq2_str[n] != "-":
335
+ end = n
336
+ break
337
+
338
+ for n in range(start, end + 1):
339
+ valid_index.append(n)
340
+
341
+ previous_index += len_dict[gene]
342
+
343
+ # for valid part
344
+ for n in valid_index:
345
+ # connected with or to evaluate insertions or deletions
346
+ if seq1_str[n] != seq2_str[n]:
347
+ identical_flag = False
348
+ else:
349
+ overlapping_cnt += 1
350
+
351
+ else:
352
+ start = 0
353
+ end = len(seq1_str) - 1
354
+ # calculate start and end
355
+ for n in range(len(seq1_str)):
356
+ if seq1_str[n] != "-" and seq2_str[n] != "-":
357
+ start = n
358
+ break
359
+
360
+ for n in range(len(seq1_str)):
361
+ if (
362
+ seq1_str[len(seq1_str) - n - 1] != "-"
363
+ and seq2_str[len(seq1_str) - n - 1] != "-"
364
+ ):
365
+ end = len(seq1_str) - n
366
+ break
367
+
368
+ # for valid part
369
+ for n in range(start, end):
370
+ # connected with or to evaluate insertions or deletions
371
+ if seq1_str[n] != seq2_str[n]:
372
+ identical_flag = False
373
+ else:
374
+ overlapping_cnt += 1
375
+
376
+ if identical_flag is True and overlapping_cnt > 0:
377
+ identical_pairs.append(
378
+ tuple(sorted([str(seq1.id).strip(), str(seq2.id).strip()]))
379
+ )
380
+ elif identical_flag is False:
381
+ different_pairs.append(
382
+ tuple(sorted([str(seq1.id).strip(), str(seq2.id).strip()]))
383
+ )
384
+
385
+ # make phylogenetic distance matrix
386
+ pdc = self.dendro_t.phylogenetic_distance_matrix().as_data_table()._data
387
+
388
+ # print(pdc)
389
+
390
+ # For each alignment identical_pairs, find tree length
391
+ for pair in identical_pairs:
392
+ if pdc[pair[0]][pair[1]] > self.zero:
393
+ # print(f"Updated zero to {pdc[pair[0]][pair[1]]} from {pair}")
394
+ self.zero = pdc[pair[0]][pair[1]]
395
+
396
+ diff_min = 999999
397
+ for pair in different_pairs:
398
+ if pdc[pair[0]][pair[1]] < diff_min:
399
+ # print(f"Updated diff_min to {pdc[pair[0]][pair[1]]} from {pair}")
400
+ diff_min = pdc[pair[0]][pair[1]]
401
+
402
+ if diff_min < self.zero:
403
+ self.zero = diff_min - 0.00000001
404
+
405
+ # I think also finding minimal distance between non-identical sequences are also needed
406
+ return self.zero
407
+
408
+ def reroot_outgroup(self, out):
409
+ # Rerooting
410
+ # Reroot should be done first because unrooted tree may have 3 children clades
411
+ outgroup_leaves = []
412
+
413
+ # Resolve polytomy before rerooting
414
+ self.t.resolve_polytomy()
415
+
416
+ # Check if outgroup sequences exists
417
+ print(f"[INFO] Rerooting {self.outgroup} in {self.tree_name}")
418
+ for leaf in self.t:
419
+ if any(outgroup.hash in leaf.name for outgroup in self.outgroup):
420
+ outgroup_leaves.append(leaf)
421
+ self.outgroup_leaf_name_list = [leaf.name for leaf in outgroup_leaves]
422
+ self.outgroup_group = list(
423
+ set(self.funinfo_dict[leaf.name].adjusted_group for leaf in outgroup_leaves)
424
+ )
425
+
426
+ # find smallest monophyletic clade that contains all leaves in outgroup_leaves
427
+ # reroot with outgroup_clade
428
+ try:
429
+ # For more than one outgroups, after rerooting, get_common_ancestor of outgroup again
430
+ if len(outgroup_leaves) >= 2:
431
+ self.outgroup_clade = self.t.get_common_ancestor(outgroup_leaves)
432
+ self.t.set_outgroup(self.outgroup_clade)
433
+ self.t.ladderize(direction=1)
434
+ self.outgroup_clade = self.t.get_common_ancestor(outgroup_leaves)
435
+ elif len(outgroup_leaves) == 1:
436
+ self.outgroup_clade = outgroup_leaves[0]
437
+ self.t.set_outgroup(self.outgroup_clade)
438
+ self.t.ladderize(direction=1)
439
+ self.outgroup_clade = outgroup_leaves[0]
440
+ else:
441
+ print(
442
+ f"{bold_red}[ERROR] no outgroup selected in {self.tree_name}{reset}"
443
+ )
444
+ raise Exception
445
+
446
+ # If number of outgroup leaves and outgroup clade does not matches, paraphyletic
447
+ if len(outgroup_leaves) != len(self.outgroup_clade):
448
+ print(
449
+ f"{yellow}[WARNING] outgroup seems to be paraphyletic in {self.tree_name}{reset}"
450
+ )
451
+
452
+ except:
453
+ print(f"{yellow}[WARNING] no outgroup selected in {self.tree_name}{reset}")
454
+
455
+ outgroup_flag = False
456
+ # if outgroup_clade is on the root side, reroot with other leaf temporarily and reroot again
457
+ for leaf in self.t:
458
+ if not (leaf in outgroup_leaves):
459
+ self.t.set_outgroup(leaf)
460
+ # Rerooting again while outgrouping gets possible
461
+ try:
462
+ self.outgroup_clade = self.t.get_common_ancestor(
463
+ outgroup_leaves
464
+ )
465
+ # print(f"Ancestor: {self.outgroup_clade}")
466
+ self.t.set_outgroup(self.outgroup_clade)
467
+ outgroup_flag = True
468
+ break
469
+ except:
470
+ pass
471
+
472
+ if outgroup_flag is False:
473
+ # never erase this for debugging
474
+ print(
475
+ f"{bold_red}[ERROR] Outgroup not selected in {self.tree_name}{reset}"
476
+ )
477
+ print(
478
+ f"{bold_red}[ERROR] local variable outgroup_leaves : {outgroup_leaves}{reset}"
479
+ )
480
+ print(f"{bold_red}[ERROR] tree_info.outgroup : {self.outgroup}{reset}")
481
+ print(
482
+ f"{bold_red}[ERROR] tree_info.outgroup_clade : {self.outgroup_clade}{reset}"
483
+ )
484
+ raise Exception
485
+
486
+ self.Tree_style.ts.show_leaf_name = True
487
+ for node in self.t.traverse():
488
+ node.img_style["size"] = 0 # removing circles whien size is 0
489
+
490
+ self.t.render(f"{out}", tree_style=self.Tree_style.ts)
491
+ self.Tree_style.ts.show_leaf_name = False
492
+
493
+ # count number of taxons in the clade
494
+ @lru_cache(maxsize=10000)
495
+ def taxon_count(self, clade, gene, count_query=False):
496
+ taxon_dict = {}
497
+
498
+ for leaf in clade:
499
+ taxon = None
500
+ if count_query == True:
501
+ taxon = (
502
+ self.funinfo_dict[leaf.name].genus,
503
+ self.funinfo_dict[leaf.name].bygene_species[gene],
504
+ )
505
+ elif (
506
+ self.decide_type(leaf.name) == "db"
507
+ or self.decide_type(leaf.name) == "outgroup"
508
+ ):
509
+ taxon = (
510
+ self.funinfo_dict[leaf.name].genus,
511
+ self.funinfo_dict[leaf.name].bygene_species[gene],
512
+ )
513
+
514
+ if not (taxon is None):
515
+ if not (taxon in taxon_dict):
516
+ taxon_dict[taxon] = 1
517
+ else:
518
+ taxon_dict[taxon] += 1
519
+
520
+ return taxon_dict
521
+
522
+ @lru_cache(maxsize=10000)
523
+ def genus_count(self, gene, clade):
524
+ taxon_dict = {}
525
+
526
+ for leaf in clade.iter_leaves():
527
+ if (
528
+ self.decide_type(leaf.name) == "db"
529
+ or self.decide_type(leaf.name) == "outgroup"
530
+ ):
531
+ if not (
532
+ (
533
+ self.funinfo_dict[leaf.name].genus,
534
+ self.funinfo_dict[leaf.name].bygene_species[gene],
535
+ )
536
+ in taxon_dict
537
+ ):
538
+ taxon_dict[
539
+ (
540
+ self.funinfo_dict[leaf.name].genus,
541
+ self.funinfo_dict[leaf.name].bygene_species[gene],
542
+ )[0]
543
+ ] = 1
544
+ else:
545
+ taxon_dict[
546
+ (
547
+ self.funinfo_dict[leaf.name].genus,
548
+ self.funinfo_dict[leaf.name].bygene_species[gene],
549
+ )[0]
550
+ ] += 1
551
+
552
+ return taxon_dict
553
+
554
+ @lru_cache(maxsize=10000)
555
+ def designate_genus(self, gene, clade):
556
+ genus_dict = self.genus_count(gene, clade)
557
+
558
+ if len(genus_dict) >= 2: # if genus is not clear
559
+ return "AMBIGUOUSGENUS"
560
+ elif len(genus_dict) == 1:
561
+ return list(genus_dict.keys())[0]
562
+ else:
563
+ return self.designate_genus(gene, clade.up)
564
+
565
+ # this function finds major species of the clade
566
+ @lru_cache(maxsize=10000)
567
+ def find_majortaxon(self, clade, gene, opt=None):
568
+ taxon_dict = self.taxon_count(clade, gene)
569
+ max_value = 0
570
+ major_taxon = ""
571
+
572
+ for taxon in taxon_dict:
573
+ if taxon_dict[taxon] > max_value:
574
+ max_value = taxon_dict[taxon]
575
+ major_taxon = taxon
576
+
577
+ if major_taxon == "":
578
+ if opt is None:
579
+ # if major species not selected, try to match genus at least
580
+ major_taxon = (
581
+ self.designate_genus(gene, clade),
582
+ f"sp. {self.sp_cnt}",
583
+ )
584
+
585
+ elif opt.mode == "validation": # in validation mode, try to follow query sp
586
+ taxon_dict = self.taxon_count(clade, gene, count_query=True)
587
+ max_value = 0
588
+ for taxon in taxon_dict:
589
+ if taxon_dict[taxon] > max_value:
590
+ max_value = taxon_dict[taxon]
591
+ major_taxon = taxon
592
+
593
+ if not (major_taxon[1].startswith("sp")):
594
+ major_taxon = (
595
+ self.designate_genus(gene, clade),
596
+ f"sp. {self.sp_cnt}",
597
+ )
598
+
599
+ else:
600
+ major_taxon = (
601
+ self.designate_genus(gene, clade),
602
+ f"sp. {self.sp_cnt}",
603
+ )
604
+
605
+ return major_taxon
606
+
607
+ def collapse(self, collapse_info, clade, taxon):
608
+ collapse_info.clade = clade
609
+ collapse_info.taxon = taxon
610
+
611
+ if len(clade) == 1:
612
+ collapse_info.collapse_type = "line"
613
+ elif len(clade) >= 2:
614
+ collapse_info.collapse_type = "triangle"
615
+ else:
616
+ raise Exception
617
+
618
+ if (
619
+ any(self.decide_type(leaf.name) == "query" for leaf in clade.iter_leaves())
620
+ == True
621
+ ):
622
+ collapse_info.color = self.opt.visualize.highlight
623
+ else:
624
+ collapse_info.color = "#000000"
625
+
626
+ # all these things were ignored when type is line
627
+ collapse_info.width = (
628
+ get_max_distance(clade) * 1000
629
+ ) # scale problem when visualizing
630
+ collapse_info.height = len(clade) * self.opt.visualize.heightmultiplier
631
+
632
+ # count query, db, others
633
+ for leaf in clade.iter_leaves():
634
+ if (
635
+ self.decide_type(leaf.name) == "db"
636
+ or self.decide_type(leaf.name) == "outgroup"
637
+ ):
638
+ collapse_info.leaf_list.append((leaf.name, "#000000", leaf.name))
639
+ collapse_info.n_db += 1
640
+ elif self.decide_type(leaf.name) == "query":
641
+ collapse_info.leaf_list.append(
642
+ (leaf.name, self.opt.visualize.highlight, leaf.name)
643
+ )
644
+ collapse_info.n_query += 1
645
+ else:
646
+ print(
647
+ f"{bold_red}[ERROR] DEVELOPMENTAL ERROR : UNEXPECTED LEAF TYPE FOR {leaf.name}{reset}"
648
+ )
649
+ print(self.tree_name)
650
+ print(f"Query: {sorted([FI.hash for FI in self.query_list])}")
651
+ print(f"DB: {sorted([FI.hash for FI in self.db_list])}")
652
+ print(f"Outgroup: {sorted([FI.hash for FI in self.outgroup])}")
653
+ raise Exception
654
+
655
+ def decide_clade(self, clade, gene):
656
+ taxon_dict = self.taxon_count(clade, gene)
657
+ if len(taxon_dict.keys()) == 0:
658
+ return "query"
659
+ else:
660
+ return "db"
661
+
662
+ # decides if the clade is monophyletic
663
+ def is_monophyletic(self, clade, gene, taxon):
664
+ taxon_dict = self.taxon_count(clade, gene)
665
+ # if taxon dict.keys() have 0 species: all query
666
+ if len(taxon_dict.keys()) == 0:
667
+ for children in clade.children:
668
+ # if any of the branch length was too long for single clade
669
+ if children.dist > self.opt.collapsedistcutoff:
670
+ return False
671
+ # or bootstrap is to distinctive
672
+ elif children.support > self.opt.collapsebscutoff:
673
+ return False
674
+ return True
675
+ elif len(taxon_dict.keys()) == 1:
676
+ # if taxon dict.keys() have only 1 species: group assigned
677
+ for children in clade.children:
678
+ # check query branch
679
+ if self.find_majortaxon(children, gene)[1].startswith("sp."):
680
+ if children.dist > self.opt.collapsedistcutoff:
681
+ return False
682
+ elif children.support > self.opt.collapsebscutoff:
683
+ return False
684
+ return True
685
+ else:
686
+ # more than 2 species : not monophyletic
687
+ return False
688
+
689
+ # Check if clade is monophyletic
690
+ def check_monophyletic(self, clade, gene):
691
+ # check if clade only has query species or not
692
+ datatype = self.decide_clade(clade, gene)
693
+
694
+ # if only one leaf in clade, it is confirmly monophyletic
695
+ if len(clade.children) == 1:
696
+ return datatype, True
697
+
698
+ # Find candidate taxon name for clade
699
+ taxon = self.find_majortaxon(clade, gene)
700
+
701
+ # Check if basal group includes query seqs
702
+ # if self.additional_clustering == False:
703
+ # self.opt.collapsedistcutoff = 0
704
+
705
+ # Check if clade is monophyletic to given taxon
706
+ if self.is_monophyletic(clade, gene, taxon):
707
+ return True
708
+ else:
709
+ return False
710
+
711
+ # Species level delimitaion on tree
712
+ def tree_search(self, clade, gene, opt=None):
713
+ def local_check_monophyletic(self, clade, gene):
714
+ # decide if given clade is clade with db or only query
715
+ def decide_clade(clade, gene):
716
+ taxon_dict = self.taxon_count(clade, gene)
717
+ if len(taxon_dict.keys()) == 0:
718
+ return "query"
719
+ else:
720
+ return "db"
721
+
722
+ # decides if the clade is monophyletic
723
+ def is_monophyletic(self, clade, gene, taxon):
724
+ taxon_dict = self.taxon_count(clade, gene)
725
+ # if taxon dict.keys() have 0 species: all query
726
+ # if any of the branch length was too long or bootstrap is to distinctive : False
727
+ if len(taxon_dict.keys()) == 0:
728
+ for children in clade.children:
729
+ if children.dist > self.opt.collapsedistcutoff:
730
+ return False
731
+ elif children.support > self.opt.collapsebscutoff:
732
+ return False
733
+ return True
734
+
735
+ # if taxon dict.keys() have only 1 species: group assigned
736
+ elif len(taxon_dict.keys()) == 1:
737
+ for children in clade.children:
738
+ if self.find_majortaxon(children, gene)[1].startswith("sp."):
739
+ if children.dist > self.opt.collapsedistcutoff:
740
+ return False
741
+ elif children.support > self.opt.collapsebscutoff:
742
+ return False
743
+ return True
744
+ else: # more than 2 species : not monophyletic
745
+ return False
746
+
747
+ # if clade only has query species or not
748
+ datatype = decide_clade(clade, gene)
749
+
750
+ # if only one clade, it is firmly monophyletic
751
+ if len(clade.children) == 1:
752
+ return datatype, True
753
+
754
+ # if additional_clustering option is on, check if basal group includes query seqs
755
+ taxon = self.find_majortaxon(clade, gene)
756
+
757
+ if is_monophyletic(self, clade, gene, taxon):
758
+ return datatype, True
759
+ else:
760
+ return datatype, False
761
+
762
+ def local_generate_collapse_information(self, clade, opt=None):
763
+ collapse_info = Collapse_information()
764
+ collapse_info.query_list = self.query_list
765
+ collapse_info.db_list = self.db_list
766
+ collapse_info.outgroup = self.outgroup
767
+ taxon = self.find_majortaxon(clade, gene, opt)
768
+ self.collapse(collapse_info, clade, taxon)
769
+
770
+ # counting new species
771
+ if taxon[1].startswith("sp."):
772
+ while 1:
773
+ self.sp_cnt += 1
774
+ if str(self.sp_cnt) in self.reserved_sp:
775
+ print(f"Skipping {self.sp_cnt} to avoid overlap in database")
776
+ continue
777
+ else:
778
+ break
779
+ """
780
+ print(
781
+ f"[INFO] Generating collapse information on {self.group} {self.gene} for taxon {taxon} ",
782
+ end="\r",
783
+ )
784
+ """
785
+
786
+ if not (taxon in self.collapse_dict):
787
+ self.collapse_dict[taxon] = [collapse_info]
788
+ else:
789
+ self.collapse_dict[taxon].append(collapse_info)
790
+
791
+ ## start of tree_search
792
+ # at the last leaf
793
+ if len(clade.children) == 1:
794
+ local_generate_collapse_information(clade, opt=opt)
795
+ return
796
+
797
+ # In bifurcated clades
798
+ elif len(clade.children) == 2:
799
+ for child_clade in clade.children:
800
+ # Calculate root distance between two childs to check flat
801
+ self.flat = (
802
+ True if child_clade.dist <= self.opt.collapsedistcutoff else False
803
+ )
804
+
805
+ # Check if child clades are monophyletic
806
+ datatype, monophyletic = local_check_monophyletic(
807
+ self, child_clade, gene
808
+ )
809
+
810
+ # If monophyletic clade, generate collapse_info and finish
811
+ if monophyletic is True:
812
+ local_generate_collapse_information(self, child_clade, opt=opt)
813
+ # Else, do recursive tree search to divide clades
814
+ else:
815
+ self.tree_search(child_clade, gene, opt=opt)
816
+ return
817
+
818
+ # if error (more than two branches or no branches)
819
+ else:
820
+ print(
821
+ f"{bold_red}[ERROR] DEVELOPMENTAL ERROR : FAILED TREE SEARCH ON LEAF {clade.children}{reset}"
822
+ )
823
+ raise Exception
824
+ # end of tree_search
825
+
826
+ # Reconstruct tree tree to solve flat branches
827
+ def reconstruct(self, clade, gene, opt):
828
+ sys.stdout.flush() # for logging
829
+
830
+ @lru_cache(maxsize=10000)
831
+ def solve_flat(clade):
832
+ # Check if the clade is consists of query db or both
833
+ def consist(c):
834
+ db, query = 0, 0
835
+ for leaf in c:
836
+ if self.decide_type(leaf.name) in ("db", "outgroup"):
837
+ db += 1
838
+ else:
839
+ query += 1
840
+
841
+ if db == 0 and query == 0:
842
+ print(
843
+ f"{bold_red}[ERROR] DEVELOPMENTAL ON CONSIST, {c} {db} {query}{reset}"
844
+ )
845
+ raise Exception
846
+ elif db == 0 and query != 0:
847
+ return "query"
848
+ elif db != 0 and query == 0:
849
+ return "db"
850
+ else:
851
+ return "both"
852
+
853
+ # Get taxon of the given clade
854
+ # c for clade (to remove redundancy to other variable: clade)
855
+ def get_taxon(c, gene, mode="db"):
856
+ # t for taxon : get taxon of the leaf
857
+ def t(leaf):
858
+ try:
859
+ return (
860
+ self.funinfo_dict[leaf.name].genus,
861
+ self.funinfo_dict[leaf.name].bygene_species[gene],
862
+ )
863
+ except:
864
+ print(
865
+ f"{bold_red}[DEVELOPMENTAL ERROR] in leaf.name tree_interpretation.py line 869 {resety}"
866
+ )
867
+ raise Exception
868
+
869
+ taxon_dict = {}
870
+
871
+ # If only db in the clade
872
+ if mode == "db":
873
+ for leaf in c:
874
+ if t(leaf) in taxon_dict:
875
+ taxon_dict[t(leaf)] += 1
876
+ else:
877
+ taxon_dict[t(leaf)] = 1
878
+
879
+ if len(taxon_dict) == 0:
880
+ print(
881
+ f"{bold_red}[DEVELOPMENTAL ERROR] in tree_interpretation.py line 884 {taxon_dict}\n {c}{reset}"
882
+ )
883
+ raise Exception
884
+ # If only one species in the clade
885
+ elif len(taxon_dict) == 1:
886
+ # If only one taxon here, return the taxon
887
+ return list(taxon_dict.keys())[0]
888
+ else:
889
+ # Else, return False
890
+ return False
891
+
892
+ # If only query in the clade
893
+ elif mode == "query":
894
+ for leaf in c:
895
+ if ("", "") in taxon_dict:
896
+ taxon_dict[("", "")] += 1
897
+ else:
898
+ taxon_dict[("", "")] = 1
899
+
900
+ """
901
+ for leaf in c:
902
+ if t in taxon_dict:
903
+ taxon_dict[t(leaf)] += 1
904
+ else:
905
+ taxon_dict[t(leaf)] = 1
906
+ """
907
+
908
+ if len(taxon_dict) == 0:
909
+ print(
910
+ f"{bold_red}[DEVELOPMENTAL ERROR] Error in tree_interpretation.py line 912 {taxon_dict}\n {c}{reset}"
911
+ )
912
+ raise Exception
913
+ elif len(taxon_dict) == 1:
914
+ return list(taxon_dict.keys())[0]
915
+ else:
916
+ max_taxon = ""
917
+ maximum = 0
918
+ for taxon in taxon_dict:
919
+ if taxon_dict[taxon] > maximum:
920
+ maximum, max_taxon = taxon_dict[taxon], taxon
921
+ return max_taxon
922
+
923
+ # If db and query mixed in the clade
924
+ elif mode == "both":
925
+ for leaf in c:
926
+ condition = False
927
+ # parse condition
928
+ if self.decide_type(leaf.name, priority="query") == "db":
929
+ condition = True
930
+ elif (
931
+ self.decide_type(leaf.name, priority="query") == "outgroup"
932
+ ):
933
+ condition = True
934
+ elif (
935
+ opt.mode == "identification"
936
+ and self.decide_type(leaf.name, priority="query") == "query"
937
+ ):
938
+ condition = True
939
+
940
+ if condition is True:
941
+ if not (t(leaf) in taxon_dict):
942
+ taxon_dict[t(leaf)] = 1
943
+ else:
944
+ taxon_dict[t(leaf)] += 1
945
+
946
+ if len(taxon_dict) == 0:
947
+ print(f"{taxon_dict}\n {c}")
948
+ raise Exception
949
+ elif len(taxon_dict) == 1:
950
+ return list(taxon_dict.keys())[0]
951
+ else:
952
+ return False
953
+
954
+ def seperate_clade(clade, gene, clade_list):
955
+ for c in clade.children:
956
+ c_tmp = c.copy()
957
+ # zero clades
958
+ if c_tmp.dist <= self.zero:
959
+ # Original version was == instead of >= . Revert if error occurs
960
+ # What does the "len" means here? -> len means number of tips
961
+ # If only one tip
962
+ if len(c_tmp) <= 1:
963
+ # In the zero branch tip, the query with zero length should move to sp., because they cannot be fully determined
964
+ clade_list.append(
965
+ (
966
+ get_taxon(c=c_tmp, gene=gene, mode=consist(c_tmp)),
967
+ c_tmp,
968
+ c_tmp.dist,
969
+ c_tmp.support,
970
+ )
971
+ )
972
+ # If more than one tip
973
+ # I'm not sure if any of the recursion enters here, but just in case
974
+ else:
975
+ clade_list = seperate_clade(
976
+ clade=c_tmp, gene=gene, clade_list=clade_list
977
+ )
978
+
979
+ # non-zero clades
980
+ else:
981
+ c2 = self.reconstruct(c_tmp, gene, opt)
982
+ clade_list.append(
983
+ (
984
+ get_taxon(c2, gene, mode=consist(c2)),
985
+ c2,
986
+ c2.dist,
987
+ c2.support,
988
+ )
989
+ )
990
+
991
+ return clade_list
992
+
993
+ ## Start of function: solve_flat
994
+ root_dist = clade.dist
995
+ root_support = clade.support
996
+
997
+ # Divide clade, all in the same level (flat branch)
998
+
999
+ clade_list = seperate_clade(clade, gene, [])
1000
+
1001
+ cnt = 0
1002
+
1003
+ # count option.zero clades
1004
+ # result is from seperate_clade function
1005
+ # each of the result has list of (taxon, clade, dist, support)
1006
+
1007
+ # count number of zero clades
1008
+ for result in clade_list:
1009
+ if result[2] <= self.zero:
1010
+ cnt += 1
1011
+
1012
+ # when entered to final leaf
1013
+ if len(clade_list) == 0:
1014
+ return clade
1015
+
1016
+ else:
1017
+ # seperating db taxon and query taxon needed
1018
+ # clade_dict format: taxon : clade
1019
+ clade_dict = {}
1020
+ final_clade = []
1021
+
1022
+ for result in clade_list:
1023
+ # if clade does not have taxonomical information
1024
+ # if final clade is mixed?
1025
+ if result[0] is False:
1026
+ final_clade.append(result[1])
1027
+
1028
+ else:
1029
+ # if new taxa
1030
+ if not (result[0] in clade_dict):
1031
+ clade_dict[result[0]] = [result]
1032
+ # if already checked taxa
1033
+ else:
1034
+ clade_dict[result[0]].append(result)
1035
+
1036
+ candidate_zero_len_taxa = set(clade_dict.keys())
1037
+
1038
+ if ("", "") in candidate_zero_len_taxa:
1039
+ if len(candidate_zero_len_taxa - set({("", "")})) == 1:
1040
+ # Move ("", "") (unknown species) to front to be combined to known species
1041
+ taxon_to_merge = sorted(list(clade_dict.keys()), reverse=False)
1042
+ else:
1043
+ # If ("", "") (unknown species) matches to multiple species, move to back to not be combined with any of the species
1044
+ taxon_to_merge = sorted(list(clade_dict.keys()), reverse=True)
1045
+ else:
1046
+ # In other case the order is not important, sort them in any way
1047
+ taxon_to_merge = sorted(list(clade_dict.keys()), reverse=False)
1048
+
1049
+ # seperate this that order should not be affected by non-zero branch
1050
+ tmp_final_clade = []
1051
+
1052
+ flat_issue_cnt = 0
1053
+ for taxon in taxon_to_merge:
1054
+ l = clade_dict[taxon] # l for list of results
1055
+ r_list = [r[1] for r in l] # result clade list
1056
+ r_list.sort(key=lambda r: r.dist, reverse=True)
1057
+ r_tuple = tuple(r_list)
1058
+
1059
+ # concatenate within taxon clades
1060
+ concatenated_clade = concat_all(
1061
+ clade_tuple=r_tuple, root_dist=0, root_support=0
1062
+ )
1063
+ tmp_final_clade.append(concatenated_clade)
1064
+
1065
+ if taxon != ("", "") and concatenated_clade.dist <= self.zero:
1066
+ flat_issue_cnt += 1
1067
+
1068
+ final_clade = tmp_final_clade + final_clade
1069
+
1070
+ final = concat_all(
1071
+ clade_tuple=tuple(final_clade),
1072
+ root_dist=root_dist,
1073
+ root_support=root_support,
1074
+ )
1075
+
1076
+ # If 2+ species are connected to flat, add flat issue
1077
+ if flat_issue_cnt >= 2:
1078
+ # print(taxon_to_merge)
1079
+ for leaf in final:
1080
+ self.flat_clades.append(leaf.name)
1081
+
1082
+ # print(f"Status flat: {self.flat_clades}")
1083
+
1084
+ return final.copy("newick")
1085
+ ## end of solve flat
1086
+
1087
+ ## Start of function reconstruct
1088
+ if len(clade.children) in (0, 1):
1089
+ return clade.copy("newick")
1090
+
1091
+ elif len(clade.children) == 2:
1092
+ clade1 = clade.children[0]
1093
+ clade2 = clade.children[1]
1094
+
1095
+ # Solve flat
1096
+ if clade.dist <= self.zero:
1097
+ return solve_flat(clade).copy("newick")
1098
+ elif clade1.dist <= self.zero or clade2.dist <= self.zero:
1099
+ return solve_flat(clade).copy("newick")
1100
+ else:
1101
+ r_clade1 = self.reconstruct(clade1, gene, opt)
1102
+ r_clade2 = self.reconstruct(clade2, gene, opt)
1103
+
1104
+ concatanated_clade = concat_clade(
1105
+ clade1=r_clade1,
1106
+ clade2=r_clade2,
1107
+ dist1=clade1.dist,
1108
+ dist2=clade2.dist,
1109
+ support1=clade1.support,
1110
+ support2=clade2.support,
1111
+ root_dist=clade.dist,
1112
+ root_support=clade.support,
1113
+ ).copy("newick")
1114
+ return concatanated_clade
1115
+
1116
+ else:
1117
+ print(f"[ERROR] {clade} {clade.children} {len(clade.children)}")
1118
+ raise Exception
1119
+ ## end of reconstruct
1120
+
1121
+ def get_bgcolor(self):
1122
+ return self.opt.visualize.backgroundcolor[
1123
+ self.bgstate % len(self.opt.visualize.backgroundcolor)
1124
+ ]
1125
+
1126
+ ### Collapse tree
1127
+ def collapse_tree(self):
1128
+ # collect taxon string to decide in polishing
1129
+ # taxon_string_list = []
1130
+ taxon_string_dict = {}
1131
+
1132
+ for collapse_taxon in self.collapse_dict:
1133
+ # collapse_info.taxon should be taxon
1134
+ for collapse_info in self.collapse_dict[collapse_taxon]:
1135
+ clade = collapse_info.clade
1136
+
1137
+ # if only one clade with same name exists
1138
+ if len(self.collapse_dict[collapse_taxon]) == 1:
1139
+ taxon_string = " ".join(collapse_info.taxon)
1140
+ taxon_string_dict[taxon_string] = (
1141
+ collapse_info.taxon[0],
1142
+ collapse_info.taxon[1],
1143
+ "",
1144
+ )
1145
+ else:
1146
+ collapse_info.clade_cnt = (
1147
+ self.collapse_dict[collapse_taxon].index(collapse_info) + 1
1148
+ )
1149
+ taxon_string = (
1150
+ f'{" ".join(collapse_info.taxon)}-{collapse_info.clade_cnt}'
1151
+ )
1152
+ taxon_string_dict[taxon_string] = (
1153
+ collapse_info.taxon[0],
1154
+ collapse_info.taxon[1],
1155
+ f"-{collapse_info.clade_cnt}",
1156
+ )
1157
+
1158
+ # taxon_string_list.append(taxon_string)
1159
+
1160
+ taxon_text = TextFace(
1161
+ taxon_string,
1162
+ fsize=self.opt.visualize.fsize,
1163
+ ftype=self.opt.visualize.ftype,
1164
+ fgcolor=collapse_info.color,
1165
+ )
1166
+
1167
+ space_text = TextFace(
1168
+ " ",
1169
+ fsize=self.opt.visualize.fsize,
1170
+ ftype=self.opt.visualize.ftype,
1171
+ fgcolor=collapse_info.color,
1172
+ )
1173
+
1174
+ # hash list for further analysis
1175
+ string_hash_list = [x[0] for x in collapse_info.leaf_list]
1176
+
1177
+ # order by translated
1178
+ string_hash_list.sort(key=lambda x: self.funinfo_dict[x].original_id)
1179
+
1180
+ id_string = divide_by_max_len(
1181
+ ",tmpseperator, ".join(string_hash_list),
1182
+ self.opt.visualize.maxwordlength,
1183
+ )
1184
+
1185
+ id_text = TextFace(
1186
+ id_string,
1187
+ fsize=self.opt.visualize.fsize,
1188
+ ftype=self.opt.visualize.ftype,
1189
+ )
1190
+
1191
+ if collapse_info.collapse_type == "triangle":
1192
+ rectangle = RectFace(
1193
+ width=collapse_info.width,
1194
+ height=collapse_info.height,
1195
+ fgcolor=collapse_info.color,
1196
+ bgcolor=collapse_info.color,
1197
+ )
1198
+ clade.add_face(rectangle, 1, position="branch-right")
1199
+
1200
+ clade.add_face(space_text, 2, position="branch-right")
1201
+ clade.add_face(taxon_text, 3, position="branch-right")
1202
+ clade.add_face(space_text, 4, position="branch-right")
1203
+ clade.add_face(id_text, 5, position="branch-right")
1204
+
1205
+ # Get all tip names of the current working clade
1206
+ collapse_leaf_name_list = [x[0] for x in collapse_info.leaf_list]
1207
+
1208
+ # Check if current working clade includes only outgroup sequences
1209
+ """
1210
+ if all(
1211
+ x in self.outgroup_leaf_name_list or x in collapse_info.query_list
1212
+ for x in collapse_leaf_name_list
1213
+ ) and any(
1214
+ x in self.outgroup_leaf_name_list for x in collapse_leaf_name_list
1215
+ ):
1216
+ """
1217
+ # Development, color unintended outgroup in outgroup color
1218
+ if any(
1219
+ self.funinfo_dict[x].adjusted_group in self.outgroup_group
1220
+ for x in collapse_leaf_name_list
1221
+ ):
1222
+ # Change background color to outgroup color
1223
+ clade.img_style["bgcolor"] = self.opt.visualize.outgroupcolor
1224
+ # Do not draw collapsed clades
1225
+ clade.img_style["draw_descendants"] = False
1226
+
1227
+ # If not outgroup sequences
1228
+ else:
1229
+ # If any of the sequence in clade considered to be in ingroup
1230
+ if any(
1231
+ self.funinfo_dict[x].adjusted_group == self.group
1232
+ for x in collapse_leaf_name_list
1233
+ ):
1234
+ # color the background
1235
+ clade.img_style["bgcolor"] = self.get_bgcolor()
1236
+ # change background color for next clade to be discrimminated
1237
+ # Currently disabled because it does not looks good
1238
+ self.bgstate += 1
1239
+ # Do not draw collapsed clades
1240
+ clade.img_style["draw_descendants"] = False
1241
+
1242
+ # show branch support above 70%
1243
+ for node in self.t.traverse():
1244
+ # change this part when debugging flat trees
1245
+ node.img_style["size"] = 0 # removing circles whien size is 0
1246
+
1247
+ if node.support >= self.opt.visualize.bscutoff:
1248
+ # node.add_face without generating extra line
1249
+ # add_face_to_node
1250
+ node.add_face(
1251
+ TextFace(
1252
+ f"{int(node.support)}",
1253
+ fsize=self.opt.visualize.fsize_bootstrap,
1254
+ fstyle="Arial",
1255
+ ),
1256
+ column=0,
1257
+ position="float",
1258
+ )
1259
+
1260
+ return taxon_string_dict
1261
+
1262
+ ### end of collapse tree
1263
+
1264
+ ### edit svg image from initial output from ete3
1265
+ def polish_image(self, out, taxon_string_dict, genus_list):
1266
+ # runname_group_gene.svg file enters here
1267
+ # the tree has rectangle collapsed group, tmpseperator, and hash
1268
+
1269
+ # Render it to temporary svg file and re-parse with xml
1270
+ self.t.render(f"{out}", tree_style=self.Tree_style.ts)
1271
+ tree_xml = ET.parse(f"{out}")
1272
+
1273
+ # in tree_xml, find all group
1274
+ _group = list(tree_xml.iter("{http://www.w3.org/2000/svg}g"))
1275
+ group_list = list(_group[0].findall("{http://www.w3.org/2000/svg}g"))
1276
+
1277
+ # in tree_xml change all rectangles to polygon (trigangle)
1278
+ for group in group_list:
1279
+ if len(list(group.findall("{http://www.w3.org/2000/svg}rect"))) == 1:
1280
+ if group.get("fill") in (
1281
+ "#000000",
1282
+ self.opt.visualize.highlight,
1283
+ ):
1284
+ rect = list(group.findall("{http://www.w3.org/2000/svg}rect"))[0]
1285
+ rect.tag = "{http://www.w3.org/2000/svg}polygon"
1286
+ rect.set(
1287
+ "points",
1288
+ f'{rect.get("width")},0 0,{int(rect.get("height"))/2} {rect.get("width")},{rect.get("height")}',
1289
+ )
1290
+
1291
+ # for taxons, gather all texts
1292
+ text_list = list(tree_xml.iter("{http://www.w3.org/2000/svg}text"))
1293
+
1294
+ # Change this module to be worked with FI hash
1295
+ for text in text_list:
1296
+ # Decide if string of the tree is bootstrap, scale, taxon or id
1297
+ # taxon_list = [" ".join(x) for x in self.collapse_dict.keys()]
1298
+ try:
1299
+ int(text.text)
1300
+ text_type = "bootstrap"
1301
+ except:
1302
+ if text.text == "0.05":
1303
+ text_type = "scale"
1304
+ elif any(
1305
+ taxon.strip() == text.text.strip()
1306
+ for taxon in taxon_string_dict.keys()
1307
+ ):
1308
+ text_type = "taxon"
1309
+ else:
1310
+ text_type = "hash"
1311
+
1312
+ # relocate text position little bit for better visualization
1313
+ text.set("y", f'{int(float(text.get("y")))-2}')
1314
+
1315
+ if text_type == "taxon":
1316
+ genus = taxon_string_dict[text.text][0]
1317
+ species = taxon_string_dict[text.text][1]
1318
+ rest = taxon_string_dict[text.text][2]
1319
+
1320
+ # split genus, species, rest of parent into tspan
1321
+ text.text = ""
1322
+ tspan_list = []
1323
+ if genus != "":
1324
+ tspan = ET.SubElement(text, "{http://www.w3.org/2000/svg}tspan")
1325
+ tspan.text = genus + " "
1326
+ tspan.set("font-style", "italic")
1327
+
1328
+ if species != "":
1329
+ tspan = ET.SubElement(text, "{http://www.w3.org/2000/svg}tspan")
1330
+ tspan.text = species
1331
+ try:
1332
+ int(species)
1333
+ except:
1334
+ if "sp." in species:
1335
+ pass
1336
+ else:
1337
+ tspan.set("font-style", "italic")
1338
+
1339
+ if rest != "":
1340
+ tspan = ET.SubElement(text, "{http://www.w3.org/2000/svg}tspan")
1341
+ tspan.text = rest + " "
1342
+
1343
+ elif text_type == "bootstrap":
1344
+ int(text.text)
1345
+ # move text a little bit higher position
1346
+ text.set("y", f'{int(text.get("y"))-8}')
1347
+ text.set("x", f'{int(text.get("x"))+1}')
1348
+
1349
+ elif text_type == "hash":
1350
+ words = text.text.split(",tmpseperator, ")
1351
+ text.text = ""
1352
+ for word in words:
1353
+ tspan = ET.SubElement(text, "{http://www.w3.org/2000/svg}tspan")
1354
+ try:
1355
+ tspan.text = self.funinfo_dict[word.strip()].original_id + " "
1356
+ if self.funinfo_dict[word.strip()].color is not None:
1357
+ try:
1358
+ tspan.set("fill", self.funinfo_dict[word.strip()].color)
1359
+ except:
1360
+ print("DEVELOPMENTAL ERROR: Failed coloring tree")
1361
+ raise Exception
1362
+
1363
+ elif self.decide_type(word, by="hash") == "query":
1364
+ tspan.set("fill", self.opt.visualize.highlight)
1365
+ except:
1366
+ pass
1367
+
1368
+ # raise Exception
1369
+
1370
+ # fit size of tree_xml to svg
1371
+ # find svg from tree_xml
1372
+ svg = list(tree_xml.iter("{http://www.w3.org/2000/svg}svg"))[0]
1373
+
1374
+ # write to svg file
1375
+ tree_xml.write(
1376
+ out,
1377
+ encoding="utf-8",
1378
+ xml_declaration=True,
1379
+ )