FunVIP 0.3.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
src/tool.py ADDED
@@ -0,0 +1,309 @@
1
+ import time, re
2
+ from Bio import SeqIO
3
+ from Bio.Seq import Seq
4
+
5
+ # from .logger import Mes
6
+ import logging
7
+ from functools import lru_cache
8
+ import sys, os
9
+ import shutil
10
+ import platform
11
+ from unidecode import unidecode
12
+
13
+
14
+ # Maybe we should move this to /funvip/src/toolbox/ and split these to multiple files
15
+ def initialize_path(path):
16
+ global genus_file
17
+ genus_file = path.genusdb
18
+
19
+
20
+ def mkdir(path):
21
+ if os.path.exists(path) == False:
22
+ os.makedirs(path)
23
+
24
+
25
+ def union_funinfo_list(funinfo_list1, funinfo_list2):
26
+ hash_list = [x.hash for x in funinfo_list1]
27
+ for funinfo in funinfo_list2:
28
+ if not funinfo.hash in hash_list:
29
+ funinfo_list1.append(funinfo)
30
+
31
+ return funinfo_list1
32
+
33
+
34
+ # if string is not ascii, automatically solve it with unicode library
35
+ def manage_unicode(string, column="", row=""):
36
+ try:
37
+ string.encode("ascii")
38
+ return string
39
+ except:
40
+ pass
41
+
42
+ if row != "":
43
+ row_string = f"{row}th row "
44
+ else:
45
+ row_string = ""
46
+
47
+ if column != "":
48
+ column_string = f"in {column} column, "
49
+ else:
50
+ column_string = ""
51
+
52
+ logging.info(
53
+ f"Illegal unicode character found {column_string}{row_string}: {unidecode(string)}. Trying flexible solve"
54
+ )
55
+
56
+ try:
57
+ return unidecode(string)
58
+ except:
59
+ logging.error(
60
+ f"Flexible solve failed to {string}. Please change the cell with available ascii strings"
61
+ )
62
+ raise Exception
63
+
64
+
65
+ @lru_cache(maxsize=10000)
66
+ def get_genus_species(
67
+ string,
68
+ endwords=[
69
+ "small",
70
+ "18S",
71
+ "ribosomal",
72
+ "internal",
73
+ "gene",
74
+ "ITS",
75
+ "5.8S",
76
+ "voucher",
77
+ "strain",
78
+ "beta",
79
+ "tubulin",
80
+ ],
81
+ genus_list=None,
82
+ ):
83
+ return_genus = ""
84
+ return_species = ""
85
+
86
+ # en for enumeratable object (splited string)
87
+ if genus_list is None:
88
+ with open(genus_file, "r") as f:
89
+ genus_list = f.read().splitlines()
90
+
91
+ en = string.replace(" ", "_").split("_")
92
+
93
+ for genus in genus_list:
94
+ for n, i in enumerate(en):
95
+ if i == genus: # or i == genus[0] or i == genus[0]+".":
96
+ return_genus = i
97
+ try:
98
+ if en[n + 2] in ["var", "var.", "f", "f.", "nom", "nom."]:
99
+ try:
100
+ en[n + 3]
101
+ if not (en[n + 3] in endwords):
102
+ return_species = " ".join(
103
+ [en[n + 1], en[n + 2], en[n + 3]]
104
+ )
105
+ else:
106
+ return_species = " ".join([en[n + 1], en[n + 2]])
107
+ except:
108
+ return_species = " ".join([en[n + 1], en[n + 2]])
109
+
110
+ else:
111
+ if en[n + 1] in ["aff", "aff.", "cf", "cf."]:
112
+ try:
113
+ if not (en[n + 2] in endwords):
114
+ return_species = " ".join([en[n + 1], en[n + 2]])
115
+ else:
116
+ return_species = en[n + 1]
117
+ except:
118
+ return_species = en[n + 1]
119
+
120
+ elif en[n + 1] in ["sp", "sp."]:
121
+ try:
122
+ if not (en[n + 2] in endwords):
123
+ try:
124
+ int(en[n + 2])
125
+ return_species = " ".join(
126
+ [en[n + 1], en[n + 2]]
127
+ )
128
+ except:
129
+ return_species = en[n + 1]
130
+ else:
131
+ return_species = en[n + 1]
132
+ except:
133
+ return_species = en[n + 1]
134
+
135
+ else:
136
+ if not (en[n + 1] in endwords):
137
+ return_species = en[n + 1]
138
+ else:
139
+ return_species = "NaN"
140
+
141
+ except:
142
+ try:
143
+ if en[n + 1] in ["sp", "sp.", "aff", "aff."]:
144
+ try:
145
+ en[n + 2]
146
+ if not (en[n + 2] in endwords):
147
+ return_species = " ".join([en[n + 1], en[n + 2]])
148
+ else:
149
+ return_species = en[n + 1]
150
+ except:
151
+ return_species = en[n + 1]
152
+ else:
153
+ if not (en[n + 1] in endwords):
154
+ return_species = en[n + 1]
155
+ else:
156
+ return_species = "NaN"
157
+ except:
158
+ return_species = "NaN"
159
+
160
+ return return_genus, return_species
161
+
162
+
163
+ # get id from string by regex match
164
+ # return message as error, not print it
165
+ @lru_cache(maxsize=10000)
166
+ def get_id(string, id_list):
167
+ id_set = set()
168
+ id_ = ""
169
+ for regex in id_list:
170
+ if re.search(regex, string):
171
+ id_set.add(re.search(regex, string).group(0))
172
+ # longest match as a best match
173
+ if len(re.search(regex, string).group(0)) > len(id_):
174
+ id_ = re.search(regex, string).group(0)
175
+ else:
176
+ pass
177
+
178
+ if len(id_set) >= 2:
179
+ logging.warning(
180
+ f"[Warning] Ambiguous regex match found. {id_} selected among {id_set}"
181
+ )
182
+
183
+ if id_ == "":
184
+ logging.warning(
185
+ f"[Warning] Cannot find regex match from {string}, using default fasta name"
186
+ )
187
+ id_ = string
188
+
189
+ id_ = str(id_)
190
+ return id_
191
+
192
+
193
+ # select FI with specific datatype from FI_list
194
+ def select(funinfo_list, datatype):
195
+ tmp_list = []
196
+ for funinfo in funinfo_list:
197
+ if funinfo.datatype == datatype:
198
+ tmp_list.append(funinfo)
199
+
200
+ return tmp_list
201
+
202
+
203
+ def excel_sheetname_legal(string):
204
+ newick_illegal = ["'", "[", "]", ":", "*", "?", "/", "\\"]
205
+ for i in newick_illegal:
206
+ string = string.replace(i, "")
207
+
208
+ string = string.replace(" ", " ")
209
+ string = string.replace(" ", "_")
210
+
211
+ return string
212
+
213
+
214
+ # moves newick files generated in tree building process to appropriate location
215
+ def cleanup_tree(path):
216
+ # move result files to each of the folders
217
+ mkdir(f"{path.out_tree}/hash")
218
+ hash_files = [
219
+ f
220
+ for f in os.listdir(f"{path.out_tree}")
221
+ if f.startswith("hash_") and f.endswith(".nwk")
222
+ ]
223
+ for file in hash_files:
224
+ try:
225
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/hash/{file}")
226
+ except:
227
+ os.remove(f"{path.out_tree}/hash/{file}")
228
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/hash/{file}")
229
+
230
+ mkdir(f"{path.out_tree}/original")
231
+ hash_files = [
232
+ f for f in os.listdir(f"{path.out_tree}") if f.endswith("_original.nwk")
233
+ ]
234
+ for file in hash_files:
235
+ try:
236
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/original/{file}")
237
+ except:
238
+ os.remove(f"{path.out_tree}/original/{file}")
239
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/original/{file}")
240
+
241
+
242
+ # moves svg files generated in CAT-V to appropriate location
243
+ def cleanup_tree_image(path):
244
+ # move result files to each of the folders
245
+ mkdir(f"{path.out_tree}/hash")
246
+ hash_files = [
247
+ f
248
+ for f in os.listdir(f"{path.out_tree}")
249
+ if f.startswith("hash_") and f.endswith(".svg")
250
+ ]
251
+ for file in hash_files:
252
+ try:
253
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/hash/{file}")
254
+ except:
255
+ os.remove(f"{path.out_tree}/hash/{file}")
256
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/hash/{file}")
257
+
258
+ mkdir(f"{path.out_tree}/original")
259
+ original_files = [
260
+ f for f in os.listdir(f"{path.out_tree}") if f.endswith("_original.svg")
261
+ ]
262
+ for file in original_files:
263
+ try:
264
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/original/{file}")
265
+ except:
266
+ os.remove(f"{path.out_tree}/original/{file}")
267
+ shutil.move(f"{path.out_tree}/{file}", f"{path.out_tree}/original/{file}")
268
+
269
+
270
+ # Change step string into numbered step
271
+ def index_step(step):
272
+ # Declaring available steps
273
+ step_list = [
274
+ "setup",
275
+ "search",
276
+ "cluster",
277
+ "align",
278
+ "trim",
279
+ "concatenate",
280
+ "modeltest",
281
+ "tree",
282
+ "visualize",
283
+ "report",
284
+ ]
285
+
286
+ try:
287
+ return step_list.index(step.lower().strip())
288
+ except:
289
+ logging.error(f"DEVELOPMENTAL ERROR, INVALID STEP {step} USED")
290
+ raise Exception
291
+
292
+
293
+ def check_avx():
294
+ return platform.machine() == "x86_64" or platform.machine() == "AMD64"
295
+
296
+
297
+ # Return logging
298
+ def get_level(level):
299
+ if level == 3:
300
+ return logging.DEBUG
301
+ elif level == 2:
302
+ return logging.INFO
303
+ elif level == 1:
304
+ return logging.WARNING
305
+ elif level == 0:
306
+ return logging.ERROR
307
+ else:
308
+ print(f"DEVELOPMENTAL ERROR ON MANAGING VERBOSE LEVEL {level}")
309
+ raise Exception
src/tree.py ADDED
@@ -0,0 +1,222 @@
1
+ from funvip.src import ext, hasher
2
+ from funvip.src.opt_generator import opt_generator
3
+ from copy import deepcopy
4
+ import multiprocessing as mp
5
+ import logging
6
+ import shutil
7
+ import os
8
+
9
+
10
+ def pipe_tree(V, path, opt, model_dict):
11
+ # for tree, use hash dict with genus and species information
12
+ tree_hash_dict = hasher.encode(V.list_FI, newick=True)
13
+
14
+ # remove tree files already exists to prevent error
15
+ try:
16
+ for file in [f for f in os.listdir(path.tmp) if f.endswith(".nwk")]:
17
+ os.remove(f"{path.tmp}/{file}")
18
+ except:
19
+ pass
20
+
21
+ try:
22
+ for file in [f for f in os.listdir(path.out_tree) if f.endswith(".nwk")]:
23
+ os.remove(f"{path.out_tree}/{file}")
24
+ except:
25
+ pass
26
+
27
+ try:
28
+ for file in [
29
+ f for f in os.listdir(f"{path.out_tree}/original/") if f.endswith(".nwk")
30
+ ]:
31
+ os.remove(f"{path.out_tree}/original/{file}")
32
+ except:
33
+ pass
34
+
35
+ try:
36
+ for file in [
37
+ f for f in os.listdir(f"{path.out_tree}/hash/") if f.endswith(".nwk")
38
+ ]:
39
+ os.remove(f"{path.out_tree}/hash/{file}")
40
+ except:
41
+ pass
42
+
43
+ fasttree_opt = [] # for multiprocessing on fasttree
44
+
45
+ tree_dataset = deepcopy(V.dict_dataset)
46
+
47
+ # Before drawing tree, finalize datasets
48
+ remove_dataset = []
49
+ for group in tree_dataset:
50
+ for gene in tree_dataset[group]:
51
+ # draw tree only when outgroup sequence exists
52
+ if tree_dataset[group][gene].list_og_FI == 0:
53
+ logging.warning(
54
+ f"Passing tree construction of {group} {gene} dataset because no outgroup available"
55
+ )
56
+ # tree_dataset[group].pop(gene, None)
57
+ remove_dataset.append((group, gene))
58
+
59
+ for group, gene in remove_dataset:
60
+ tree_dataset[group].pop(gene, None)
61
+
62
+ # To indicate single genes for each group
63
+ singlegene_dict = {}
64
+
65
+ # Draw phylogenetic trees for each dataset
66
+ for group in tree_dataset:
67
+ singlegene_flag = 0
68
+ singlegene = ""
69
+ # For single gene, we don't have to draw tree multiple times
70
+ if len(tree_dataset[group].keys()) == 2:
71
+ singlegene_flag = 1
72
+ singlegene = list(set(tree_dataset[group].keys()) - {"concatenated"})[0]
73
+ singlegene_dict[group] = deepcopy(singlegene)
74
+
75
+ for gene in tree_dataset[group]:
76
+ # For single gene, skip concatenated
77
+ if gene == "concatenated" and singlegene_flag == 1:
78
+ logging.info(
79
+ f"Passing tree construction of {group} {gene} because single gene detected. Will copy from concatenated"
80
+ )
81
+ # Else for each gene
82
+ elif gene != "concatenated":
83
+ # If not trimming use this
84
+ if opt.method.tree.lower() == "raxml":
85
+ ext.RAxML(
86
+ fasta=f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
87
+ out=f"{opt.runname}_{group}_{gene}.nwk",
88
+ hash_dict=tree_hash_dict,
89
+ path=path,
90
+ thread=opt.thread,
91
+ bootstrap=opt.bootstrap,
92
+ model=model_dict[group][gene],
93
+ )
94
+
95
+ elif opt.method.tree.lower() == "iqtree":
96
+ ext.IQTREE(
97
+ fasta=f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
98
+ out=f"{opt.runname}_{group}_{gene}.nwk",
99
+ hash_dict=tree_hash_dict,
100
+ path=path,
101
+ memory=opt.memory,
102
+ thread=opt.thread,
103
+ bootstrap=opt.bootstrap,
104
+ model=model_dict[group][gene],
105
+ )
106
+
107
+ # if fasttree, append to opt to perform multiprocessing by each tree
108
+ else:
109
+ if not (opt.method.tree.lower() == "fasttree"):
110
+ logging.warning(
111
+ "Tree construction method not selected, working for default option, FastTree"
112
+ )
113
+ fasttree_opt.append(
114
+ (
115
+ f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
116
+ f"{opt.runname}_{group}_{gene}.nwk",
117
+ tree_hash_dict,
118
+ path,
119
+ model_dict[group][gene],
120
+ )
121
+ )
122
+ # For concatenated datasets, use partition files
123
+ else:
124
+ if opt.method.tree.lower() == "raxml":
125
+ ext.RAxML(
126
+ fasta=f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
127
+ out=f"{opt.runname}_{group}_{gene}.nwk",
128
+ hash_dict=tree_hash_dict,
129
+ path=path,
130
+ thread=opt.thread,
131
+ bootstrap=opt.bootstrap,
132
+ partition=f"{path.out_alignment}/{opt.runname}_{group}.partition",
133
+ model=model_dict[group][gene],
134
+ )
135
+
136
+ elif opt.method.tree.lower() == "iqtree":
137
+ ext.IQTREE(
138
+ fasta=f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
139
+ out=f"{opt.runname}_{group}_{gene}.nwk",
140
+ hash_dict=tree_hash_dict,
141
+ path=path,
142
+ memory=opt.memory,
143
+ thread=opt.thread,
144
+ bootstrap=opt.bootstrap,
145
+ partition=f"{path.out_alignment}/{opt.runname}_{group}.partition",
146
+ model=model_dict[group][gene],
147
+ )
148
+
149
+ else:
150
+ if not (opt.method.tree.lower() == "fasttree"):
151
+ logging.warning(
152
+ "Tree method not selected, working for default option, FastTree"
153
+ )
154
+ fasttree_opt.append(
155
+ (
156
+ f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
157
+ f"{opt.runname}_{group}_{gene}.nwk",
158
+ tree_hash_dict,
159
+ path,
160
+ model_dict[group][gene],
161
+ )
162
+ )
163
+
164
+ # for fasttree, perform multiprocessing
165
+ if opt.method.tree.lower() == "fasttree":
166
+ # run multiprocessing start
167
+ if opt.verbose < 3:
168
+ p = mp.Pool(opt.thread)
169
+ fasttree_result = p.starmap(ext.FastTree, fasttree_opt)
170
+ p.close()
171
+ p.join()
172
+
173
+ else:
174
+ # non-multithreading mode for debugging
175
+ fasttree_result = []
176
+ for option in fasttree_opt:
177
+ fasttree_result.append(ext.FastTree(*option))
178
+
179
+ for group in singlegene_dict:
180
+ # copy concatenated result to single gene
181
+ shutil.copy(
182
+ f"{path.out_tree}/{opt.runname}_{group}_{singlegene_dict[group]}.nwk",
183
+ f"{path.out_tree}/{opt.runname}_{group}_concatenated.nwk",
184
+ )
185
+
186
+ shutil.copy(
187
+ f"{path.out_tree}/hash_{opt.runname}_{group}_{singlegene_dict[group]}.nwk",
188
+ f"{path.out_tree}/hash_{opt.runname}_{group}_concatenated.nwk",
189
+ )
190
+
191
+ ## Decode alignments and trimmed alignments for each gene after building tree
192
+ # If the code has been mature enough, move this to right after modeltest
193
+ for group in tree_dataset:
194
+ for gene in tree_dataset[group]:
195
+ try:
196
+ if gene != "concatenated":
197
+ os.rename(
198
+ f"{path.out_alignment}/{opt.runname}_MAFFT_{group}_{gene}.fasta",
199
+ f"{path.out_alignment}/hash/{opt.runname}_hash_MAFFT_{group}_{gene}.fasta",
200
+ )
201
+ hasher.decode(
202
+ tree_hash_dict,
203
+ f"{path.out_alignment}/hash/{opt.runname}_hash_MAFFT_{group}_{gene}.fasta",
204
+ f"{path.out_alignment}/{opt.runname}_MAFFT_{group}_{gene}.fasta",
205
+ )
206
+
207
+ os.rename(
208
+ f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
209
+ f"{path.out_alignment}/hash/{opt.runname}_hash_trimmed_{group}_{gene}.fasta",
210
+ )
211
+
212
+ hasher.decode(
213
+ tree_hash_dict,
214
+ f"{path.out_alignment}/hash/{opt.runname}_hash_trimmed_{group}_{gene}.fasta",
215
+ f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
216
+ )
217
+ except:
218
+ logging.warning(
219
+ f"Tried decoding alignments of {group} {gene} but failed"
220
+ )
221
+
222
+ return V, path, opt