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
|
@@ -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
|
+
)
|