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
src/save.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# For save and load functions
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import numpy as np
|
|
10
|
+
import logging
|
|
11
|
+
from unidecode import unidecode
|
|
12
|
+
from Bio import SeqIO
|
|
13
|
+
from funvip.src.tool import (
|
|
14
|
+
initialize_path,
|
|
15
|
+
get_genus_species,
|
|
16
|
+
get_id,
|
|
17
|
+
manage_unicode,
|
|
18
|
+
)
|
|
19
|
+
from funvip.src.logics import isnewicklegal
|
|
20
|
+
from funvip.src.hasher import decode, newick_legal, hash_funinfo_list
|
|
21
|
+
import shelve
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Saving functions should be in main branch because of globals() function
|
|
25
|
+
# try move it to other place by sending globals() as variable
|
|
26
|
+
# In future, try selectively save to reduce datasize
|
|
27
|
+
# Session saving function
|
|
28
|
+
def save_session(opt, path, global_var: dict, var: dict) -> None:
|
|
29
|
+
managed_keys = ("V", "R", "opt", "path", "model_dict")
|
|
30
|
+
|
|
31
|
+
# if opt.save_run is True:
|
|
32
|
+
save = shelve.open(path.save, "n")
|
|
33
|
+
|
|
34
|
+
for key in global_var:
|
|
35
|
+
if key in managed_keys:
|
|
36
|
+
save[key] = global_var[key]
|
|
37
|
+
if opt.verbose >= 3:
|
|
38
|
+
logging.debug(f"Saved {key}")
|
|
39
|
+
else:
|
|
40
|
+
if opt.verbose >= 3:
|
|
41
|
+
logging.debug(f"Did not saved {key}")
|
|
42
|
+
|
|
43
|
+
logging.info(f"Saved current session")
|
|
44
|
+
save.close()
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
if 1: # ADD saving options in further developmental stage
|
|
48
|
+
save = shelve.open(path.save, "n")
|
|
49
|
+
for key in global_var:
|
|
50
|
+
try:
|
|
51
|
+
save[key] = global_var[key]
|
|
52
|
+
if opt.verbose >= 3:
|
|
53
|
+
logging.debug(f"Saved {key}")
|
|
54
|
+
except:
|
|
55
|
+
if opt.verbose >= 3:
|
|
56
|
+
logging.debug(f"Failed shelving: {key}")
|
|
57
|
+
logging.info(f"Saved current session")
|
|
58
|
+
save.close()
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Session loading function
|
|
63
|
+
def load_session(opt, savefile: str) -> None:
|
|
64
|
+
var = {}
|
|
65
|
+
save = shelve.open(savefile)
|
|
66
|
+
for key in save:
|
|
67
|
+
try:
|
|
68
|
+
var[key] = save[key]
|
|
69
|
+
if opt.verbose >= 3:
|
|
70
|
+
logging.debug(f"Loading {key}")
|
|
71
|
+
except:
|
|
72
|
+
if opt.verbose >= 3:
|
|
73
|
+
logging.debug(f"Failed Loading {key}")
|
|
74
|
+
|
|
75
|
+
logging.info(f"Loaded previous session")
|
|
76
|
+
save.close()
|
|
77
|
+
|
|
78
|
+
return var
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_fasta(list_funinfo, gene, filename, by="id"):
|
|
82
|
+
list_funinfo = list(set(list_funinfo)) # remove ambiguous seqs
|
|
83
|
+
|
|
84
|
+
with open(f"{filename}", "w") as fp:
|
|
85
|
+
if gene == "unclassified": # for unclassified query
|
|
86
|
+
flag = 0
|
|
87
|
+
for info in list_funinfo:
|
|
88
|
+
for n, seq in enumerate(info.unclassified_seq):
|
|
89
|
+
if by == "hash":
|
|
90
|
+
fp.write(f">{info.hash}_{n}\n{seq}\n")
|
|
91
|
+
else:
|
|
92
|
+
fp.write(f">{info.id}_{n}\n{seq}\n")
|
|
93
|
+
flag = 1
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
flag = 0
|
|
97
|
+
for info in list_funinfo:
|
|
98
|
+
if gene in info.seq:
|
|
99
|
+
if len(info.seq[gene]) > 0:
|
|
100
|
+
if by == "hash":
|
|
101
|
+
fp.write(f">{info.hash}\n{info.seq[gene]}\n")
|
|
102
|
+
else:
|
|
103
|
+
fp.write(f">{info.id}\n{info.seq[gene]}\n")
|
|
104
|
+
flag = 1
|
|
105
|
+
|
|
106
|
+
# returns 1 if meaningful sequence exists
|
|
107
|
+
return flag
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def save_originalfasta(list_info, path, filename):
|
|
111
|
+
with open(f"{path}/{filename}", "w") as fp:
|
|
112
|
+
for info in list_info:
|
|
113
|
+
fp.write(f">{info.description}\n{info.seq}\n")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def save_fastabygroup(list_funinfo, path, option, add="Reference", outgroup=False):
|
|
117
|
+
outpath = path.data
|
|
118
|
+
set_group = set()
|
|
119
|
+
|
|
120
|
+
for group in set_group:
|
|
121
|
+
tmp_list = []
|
|
122
|
+
for funinfo in list_funinfo:
|
|
123
|
+
if funinfo.adjusted_group == group:
|
|
124
|
+
tmp_list.append(funinfo)
|
|
125
|
+
|
|
126
|
+
save_fasta(tmp_list, outpath, f"{add}_{group}.fasta")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Save tree file to designated path, and decode it
|
|
130
|
+
def save_tree(out, hash_dict, hash_file_path, decoded_file_path, fix=False):
|
|
131
|
+
# print(out)
|
|
132
|
+
file = out.split("/")[-1]
|
|
133
|
+
shutil.move(out, hash_file_path)
|
|
134
|
+
decode(
|
|
135
|
+
hash_dict=hash_dict,
|
|
136
|
+
file=hash_file_path,
|
|
137
|
+
out=decoded_file_path,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# In fasttree result, 0 supports are not shown. so fix it
|
|
141
|
+
if fix is True:
|
|
142
|
+
with open(hash_file_path, "r") as fr:
|
|
143
|
+
newick = fr.read()
|
|
144
|
+
|
|
145
|
+
with open(hash_file_path, "w") as fw:
|
|
146
|
+
fw.write(newick.replace("):", ")0.0:"))
|
|
147
|
+
|
|
148
|
+
with open(decoded_file_path, "r") as fr:
|
|
149
|
+
newick = fr.read()
|
|
150
|
+
|
|
151
|
+
with open(decoded_file_path, "w") as fw:
|
|
152
|
+
fw.write(newick.replace("):", ")0.0:"))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def save_mergedfasta(fasta_list, out_path):
|
|
156
|
+
out_fasta_list = []
|
|
157
|
+
for fasta in fasta_list:
|
|
158
|
+
out_fasta_list += list(SeqIO.parse(fasta, "fasta"))
|
|
159
|
+
|
|
160
|
+
SeqIO.write(out_fasta_list, out_path, "fasta")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# save dataframe
|
|
164
|
+
def save_df(df, out, fmt="csv"):
|
|
165
|
+
if fmt == "csv" or fmt == "tsv":
|
|
166
|
+
df.to_csv(out, index=False)
|
|
167
|
+
# For excel size limit
|
|
168
|
+
elif fmt == "xlsx" or fmt == "excel":
|
|
169
|
+
# Limit is 1048576 rows with 16384 column, exlcuding one for column
|
|
170
|
+
if len(df) <= 1048575 and df.shape[1] <= 16383:
|
|
171
|
+
df.to_excel(out, index=False)
|
|
172
|
+
else:
|
|
173
|
+
logging.warning(f"Dataframe size exceeds excel limit. Using csv instead")
|
|
174
|
+
df.to_csv(out, index=False)
|
|
175
|
+
elif fmt == "parquet":
|
|
176
|
+
df.to_parquet(out, index=False)
|
|
177
|
+
elif fmt == "feather" or fmt == "ftr":
|
|
178
|
+
df.to_feather(out, index=False)
|
|
179
|
+
else:
|
|
180
|
+
logging.warning(f"Matrix format not valid. Using csv as default")
|
|
181
|
+
df.to_csv(out, index=False)
|
src/search.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
from funvip.src import cluster, tool, hasher, validate_input, save
|
|
2
|
+
from funvip.src.ext import blast, makeblastdb, mmseqs, makemmseqsdb
|
|
3
|
+
from funvip.src.save import save_df
|
|
4
|
+
import copy
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
import shutil
|
|
11
|
+
import hashlib
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cleanblastdb(db):
|
|
15
|
+
os.remove(f"{db}.nsq")
|
|
16
|
+
os.remove(f"{db}.nin")
|
|
17
|
+
os.remove(f"{db}.nhr")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cleanmmseqsdb(db):
|
|
21
|
+
os.remove(f"{db}.dbtype")
|
|
22
|
+
os.remove(f"{db}_h.dbtype")
|
|
23
|
+
os.remove(f"{db}.index")
|
|
24
|
+
os.remove(f"{db}_h.index")
|
|
25
|
+
os.remove(f"{db}.lookup")
|
|
26
|
+
os.remove(f"{db}.source")
|
|
27
|
+
os.remove(f"{db}")
|
|
28
|
+
os.remove(f"{db}_h")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Make blast db file or mmseqs db file
|
|
32
|
+
def create_search_db(opt, db_fasta, db, path) -> None:
|
|
33
|
+
# make blastdb
|
|
34
|
+
if opt.method.search.lower() in ("blast", "blastn"):
|
|
35
|
+
makeblastdb(fasta=db_fasta, db=db, path=path)
|
|
36
|
+
# make mmseqssdb
|
|
37
|
+
elif opt.method.search.lower() in (
|
|
38
|
+
"mmseq",
|
|
39
|
+
"mmseqss",
|
|
40
|
+
"mmseqs",
|
|
41
|
+
"mmseq2",
|
|
42
|
+
"mmseqs2",
|
|
43
|
+
"mmseqss2",
|
|
44
|
+
):
|
|
45
|
+
makemmseqsdb(fasta=db_fasta, db=db, path=path)
|
|
46
|
+
else:
|
|
47
|
+
logging.error("DEVELOPMENTAL ERROR on building search DB!")
|
|
48
|
+
raise Exception
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Merge fragmented search matches from given blast or mmseqss results
|
|
52
|
+
def merge_fragments(df) -> pd.DataFrame():
|
|
53
|
+
# Check if list (or pandas series or one-column DataFrame) has only one value
|
|
54
|
+
def get_unique(series) -> str:
|
|
55
|
+
if len(set(series)) == 1:
|
|
56
|
+
return list(series)[0]
|
|
57
|
+
elif len(set(series)) == 0:
|
|
58
|
+
logging.error(f"Found 0 values in {series} while merging search results")
|
|
59
|
+
raise Exception
|
|
60
|
+
else:
|
|
61
|
+
logging.error(
|
|
62
|
+
f"Found {len(set(series))} values in {series} while merging search results"
|
|
63
|
+
)
|
|
64
|
+
raise Exception
|
|
65
|
+
|
|
66
|
+
# Calculate overall percent identity of fragments
|
|
67
|
+
def calculate_pident(df):
|
|
68
|
+
pident = df["pident"]
|
|
69
|
+
length = df["length"]
|
|
70
|
+
if len(pident) != len(length):
|
|
71
|
+
logging.error(
|
|
72
|
+
f"During merge blast fragments, found pident {pident} and len {length} are different"
|
|
73
|
+
)
|
|
74
|
+
raise Exception
|
|
75
|
+
elif len(pident) == 0:
|
|
76
|
+
logging.error(f"No percent identitiy {pident} found")
|
|
77
|
+
raise Exception
|
|
78
|
+
else:
|
|
79
|
+
# Calculate overall percent identity
|
|
80
|
+
return np.sum(np.array(pident) * np.array(length)) / np.sum(length)
|
|
81
|
+
|
|
82
|
+
# Return empty df because if causes error
|
|
83
|
+
if len(df.index) == 0: # faster way for df.empty
|
|
84
|
+
return df
|
|
85
|
+
|
|
86
|
+
# pident needs access to other columns, will be calculated in next line
|
|
87
|
+
# qseqid and sseqid will return itself, after checking if they are unique
|
|
88
|
+
# mismatch, gaps, and bitscore were calculated as sum of fragments
|
|
89
|
+
# evalues were calculated by multiplying them, because they are probability
|
|
90
|
+
|
|
91
|
+
df = df.groupby(["qseqid", "sseqid"], dropna=True, as_index=False).aggregate(
|
|
92
|
+
{
|
|
93
|
+
"qseqid": lambda x: set(x),
|
|
94
|
+
"sseqid": lambda x: set(x),
|
|
95
|
+
"pident": lambda x: tuple(x),
|
|
96
|
+
"length": lambda x: tuple(x),
|
|
97
|
+
"mismatch": np.sum,
|
|
98
|
+
"gaps": np.sum,
|
|
99
|
+
"qstart": lambda x: tuple(x),
|
|
100
|
+
"qend": lambda x: tuple(x),
|
|
101
|
+
"sstart": lambda x: tuple(x),
|
|
102
|
+
"send": lambda x: tuple(x),
|
|
103
|
+
"evalue": np.prod,
|
|
104
|
+
"bitscore": np.sum,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Calculate pident
|
|
109
|
+
df["pident"] = df.apply(calculate_pident, axis=1)
|
|
110
|
+
|
|
111
|
+
# Merge length
|
|
112
|
+
df["length"] = df["length"].apply(sum).astype(int)
|
|
113
|
+
|
|
114
|
+
# Update qseqid and sseqid
|
|
115
|
+
df["qseqid"] = df["qseqid"].apply(lambda x: tuple(x)[0])
|
|
116
|
+
df["sseqid"] = df["sseqid"].apply(lambda x: tuple(x)[0])
|
|
117
|
+
|
|
118
|
+
return df
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Core search : Running blast or mmseqss
|
|
122
|
+
def search(query_fasta, db_fasta, path, opt) -> pd.DataFrame():
|
|
123
|
+
# Working on saved database
|
|
124
|
+
# find key for which db to use
|
|
125
|
+
# get hash number of the given db to compare
|
|
126
|
+
_hash = hashlib.md5(open(db_fasta, "rb").read()).hexdigest()
|
|
127
|
+
|
|
128
|
+
if opt.cachedb is True or opt.usecache is True:
|
|
129
|
+
if _hash is None:
|
|
130
|
+
logging.error(f"Database file {db_fasta} missing")
|
|
131
|
+
raise Exception
|
|
132
|
+
|
|
133
|
+
# Try to parse DB
|
|
134
|
+
if opt.usecache is True:
|
|
135
|
+
# When succesfully parsed DB
|
|
136
|
+
if (
|
|
137
|
+
os.path.isdir(f"{path.in_db}/{opt.method.search.lower()}/{_hash}")
|
|
138
|
+
is True
|
|
139
|
+
):
|
|
140
|
+
logging.info("[INFO] Found existing database! Skipping database build")
|
|
141
|
+
db = f"{path.in_db}/{opt.method.search.lower()}/{_hash}/{_hash}"
|
|
142
|
+
|
|
143
|
+
# When parsing existing DB failed
|
|
144
|
+
else:
|
|
145
|
+
logging.info("No existing database found")
|
|
146
|
+
|
|
147
|
+
# Try save DB
|
|
148
|
+
if opt.cachedb is True:
|
|
149
|
+
logging.info(
|
|
150
|
+
f"--cachedb selected, {opt.method.search.lower()} database will be saved"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Create DB saving directory
|
|
154
|
+
os.mkdir(f"{path.in_db}/{opt.method.search.lower()}/{_hash}")
|
|
155
|
+
db = f"{path.in_db}/{opt.method.search.lower()}/{_hash}/{_hash}"
|
|
156
|
+
|
|
157
|
+
# DB saving starts
|
|
158
|
+
logging.info("The database is in first run, caching database")
|
|
159
|
+
|
|
160
|
+
# Create search database
|
|
161
|
+
create_search_db(opt, db_fasta, db, path)
|
|
162
|
+
|
|
163
|
+
# Passing Save DB
|
|
164
|
+
else:
|
|
165
|
+
logging.info(
|
|
166
|
+
"--cachedb not selected, saving database will be passed"
|
|
167
|
+
)
|
|
168
|
+
os.mkdir(f"{path.tmp}/{opt.runname}/{_hash}")
|
|
169
|
+
db = f"{path.tmp}/{opt.runname}/{_hash}/{_hash}"
|
|
170
|
+
|
|
171
|
+
# Create search database
|
|
172
|
+
create_search_db(opt, db_fasta, db, path)
|
|
173
|
+
|
|
174
|
+
else:
|
|
175
|
+
os.mkdir(f"{path.tmp}/{opt.runname}/{_hash}")
|
|
176
|
+
db = f"{path.tmp}/{opt.runname}/{_hash}/{_hash}"
|
|
177
|
+
# Create search database
|
|
178
|
+
create_search_db(opt, db_fasta, db, path)
|
|
179
|
+
|
|
180
|
+
# run searching
|
|
181
|
+
# blast
|
|
182
|
+
if opt.method.search.lower() in ("blast", "blastn"):
|
|
183
|
+
blast_result = blast(
|
|
184
|
+
query=query_fasta,
|
|
185
|
+
db=db,
|
|
186
|
+
out=f"{path.tmp}/{opt.runname}.m8",
|
|
187
|
+
path=path,
|
|
188
|
+
opt=opt,
|
|
189
|
+
)
|
|
190
|
+
# remove db when saving not enabled
|
|
191
|
+
# Temporarily disabled to remove bug
|
|
192
|
+
# if opt.cachedb is False:
|
|
193
|
+
# cleanblastdb(db)
|
|
194
|
+
|
|
195
|
+
# mmseqs
|
|
196
|
+
elif opt.method.search.lower() in ("mmseqs", "mmseq", "mmseq2", "mmseqs2"):
|
|
197
|
+
mmseqs(
|
|
198
|
+
query=query_fasta,
|
|
199
|
+
db=db,
|
|
200
|
+
out=f"{path.tmp}/{opt.runname}.m8",
|
|
201
|
+
tmp=path.tmp,
|
|
202
|
+
path=path,
|
|
203
|
+
opt=opt,
|
|
204
|
+
)
|
|
205
|
+
# remove db when saving not enabled
|
|
206
|
+
# Temporarily disabled to remove bug
|
|
207
|
+
# if opt.cachedb is False:
|
|
208
|
+
# cleanmmseqsdb(db)
|
|
209
|
+
# remove temporary file
|
|
210
|
+
# shutil.rmtree(f"{path.tmp}/{opt.runname}")
|
|
211
|
+
else:
|
|
212
|
+
logging.error("DEVELOPMENTAL ERROR on searching!")
|
|
213
|
+
raise Exception
|
|
214
|
+
|
|
215
|
+
# Parse out
|
|
216
|
+
df = pd.read_csv(
|
|
217
|
+
f"{path.tmp}/{opt.runname}.m8",
|
|
218
|
+
sep="\t",
|
|
219
|
+
header=None,
|
|
220
|
+
names=[
|
|
221
|
+
"qseqid",
|
|
222
|
+
"sseqid",
|
|
223
|
+
"pident",
|
|
224
|
+
"length",
|
|
225
|
+
"mismatch",
|
|
226
|
+
"gaps",
|
|
227
|
+
"qstart",
|
|
228
|
+
"qend",
|
|
229
|
+
"sstart",
|
|
230
|
+
"send",
|
|
231
|
+
"evalue",
|
|
232
|
+
"bitscore",
|
|
233
|
+
],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Remove temporary files
|
|
237
|
+
os.remove(f"{path.tmp}/{opt.runname}.m8")
|
|
238
|
+
|
|
239
|
+
# merge fragmented matches
|
|
240
|
+
df = merge_fragments(df)
|
|
241
|
+
|
|
242
|
+
return df
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
### Main search: Get blast or mmseqs dataframe results from given dataset
|
|
246
|
+
def search_df(V, path, opt):
|
|
247
|
+
# Initialize variable for search result
|
|
248
|
+
dict_search = {}
|
|
249
|
+
|
|
250
|
+
# Ready for by sseqid hash, which group to append
|
|
251
|
+
# generating group dict to assign group column next to the output dataframe
|
|
252
|
+
group_dict = {}
|
|
253
|
+
for FI in V.list_FI:
|
|
254
|
+
group_dict[FI.hash] = FI.group
|
|
255
|
+
|
|
256
|
+
# genes available for db and query
|
|
257
|
+
V.list_db_gene = copy.copy(opt.gene)
|
|
258
|
+
V.list_qr_gene = copy.copy(opt.gene)
|
|
259
|
+
|
|
260
|
+
# make blastdb for this run
|
|
261
|
+
list_db_FI = tool.select(V.list_FI, datatype="db")
|
|
262
|
+
|
|
263
|
+
# Make database by genes
|
|
264
|
+
for gene in opt.gene:
|
|
265
|
+
db_state = save.save_fasta(
|
|
266
|
+
list_funinfo=list_db_FI,
|
|
267
|
+
gene=gene,
|
|
268
|
+
filename=f"{path.tmp}/{opt.runname}_DB_{gene}.fasta",
|
|
269
|
+
by="hash",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if db_state == 0:
|
|
273
|
+
logging.warning(
|
|
274
|
+
f"No data for {gene} found in database. Excluding from analysis"
|
|
275
|
+
)
|
|
276
|
+
V.list_db_gene.remove(gene)
|
|
277
|
+
|
|
278
|
+
if len(V.list_db_gene) == 0:
|
|
279
|
+
logging.error(
|
|
280
|
+
f"None of the gene seems to be valid in analysis. Please check your --gene flag"
|
|
281
|
+
)
|
|
282
|
+
raise Exception
|
|
283
|
+
|
|
284
|
+
# get query fasta from funinfo_list
|
|
285
|
+
list_qr_FI = tool.select(V.list_FI, datatype="query")
|
|
286
|
+
|
|
287
|
+
# hash_dict format - hash : id
|
|
288
|
+
V.dict_id_hash = hasher.encode(V.list_FI)
|
|
289
|
+
|
|
290
|
+
# if no query exists and only database sequence exists
|
|
291
|
+
if len(list_qr_FI) == 0:
|
|
292
|
+
logging.warning("No query selected. Changing to database validation mode")
|
|
293
|
+
# db by db search analysis
|
|
294
|
+
for gene in V.list_db_gene:
|
|
295
|
+
df_search = search(
|
|
296
|
+
query_fasta=f"{path.tmp}/{opt.runname}_DB_{gene}.fasta",
|
|
297
|
+
db_fasta=f"{path.tmp}/{opt.runname}_DB_{gene}.fasta",
|
|
298
|
+
path=path,
|
|
299
|
+
opt=opt,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Cutoff by outgroupcutoff
|
|
303
|
+
df_search = df_search[df_search["bitscore"] > opt.cluster.outgroupoffset]
|
|
304
|
+
|
|
305
|
+
# append group column
|
|
306
|
+
df_search["subject_group"] = df_search["sseqid"].apply(
|
|
307
|
+
lambda x: group_dict.get(x)
|
|
308
|
+
)
|
|
309
|
+
# add to search dict
|
|
310
|
+
# dict_search[gene] = df_search
|
|
311
|
+
V.dict_gene_SR[gene] = df_search
|
|
312
|
+
|
|
313
|
+
# Save dataframe
|
|
314
|
+
if opt.nosearchresult is False:
|
|
315
|
+
save_df(
|
|
316
|
+
hasher.decode_df(V.dict_id_hash, df_search),
|
|
317
|
+
f"{path.out_matrix}/{opt.runname}_BLAST_result_{gene}.{opt.tableformat}",
|
|
318
|
+
fmt=opt.tableformat,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# if query sequence exists
|
|
322
|
+
else:
|
|
323
|
+
query_state = save.save_fasta(
|
|
324
|
+
list_qr_FI,
|
|
325
|
+
"unclassified",
|
|
326
|
+
f"{path.tmp}/{opt.runname}_Query_unclassified.fasta",
|
|
327
|
+
by="hash",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# first, run for unclassified query for assign gene
|
|
331
|
+
dict_unclassified = {}
|
|
332
|
+
|
|
333
|
+
if query_state == 1: # if unclassified sequence exists
|
|
334
|
+
for gene in V.list_db_gene:
|
|
335
|
+
df_search = search(
|
|
336
|
+
query_fasta=f"{path.tmp}/{opt.runname}_Query_unclassified.fasta",
|
|
337
|
+
db_fasta=f"{path.tmp}/{opt.runname}_DB_{gene}.fasta",
|
|
338
|
+
path=path,
|
|
339
|
+
opt=opt,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Cutoff by outgroupcutoff
|
|
343
|
+
print(vars(opt))
|
|
344
|
+
print(opt.cluster.outgroupoffset)
|
|
345
|
+
df_search = df_search[
|
|
346
|
+
df_search["bitscore"] > opt.cluster.outgroupoffset
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
logging.debug(f"{df_search}")
|
|
350
|
+
|
|
351
|
+
if len(df_search) == 0:
|
|
352
|
+
del df_search
|
|
353
|
+
logging.debug(
|
|
354
|
+
f"Deleted df because non of the result exceeds outgroupoffset!"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# if dataframe exists
|
|
358
|
+
try:
|
|
359
|
+
if isinstance(df_search, pd.DataFrame):
|
|
360
|
+
if not df_search.empty:
|
|
361
|
+
dict_unclassified[gene] = df_search
|
|
362
|
+
except:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
# assign gene by search result to unassigned sequences
|
|
366
|
+
V = cluster.assign_gene(dict_unclassified, V)
|
|
367
|
+
|
|
368
|
+
# then, gene by gene BLAST for outgroup
|
|
369
|
+
for gene in opt.gene:
|
|
370
|
+
# if database is very confident, so no more validation is required
|
|
371
|
+
if opt.confident is True:
|
|
372
|
+
query_state = save.save_fasta(
|
|
373
|
+
[FI for FI in V.list_FI if FI.datatype == "query"],
|
|
374
|
+
gene,
|
|
375
|
+
f"{path.tmp}/{opt.runname}_Query_{gene}.fasta",
|
|
376
|
+
by="hash",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# for most of the cases
|
|
380
|
+
else:
|
|
381
|
+
query_state = save.save_fasta(
|
|
382
|
+
V.list_FI,
|
|
383
|
+
gene,
|
|
384
|
+
f"{path.tmp}/{opt.runname}_Query_{gene}.fasta",
|
|
385
|
+
by="hash",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# if no sequence exists for gene, remove it
|
|
389
|
+
if query_state == 0:
|
|
390
|
+
V.list_qr_gene.remove(gene)
|
|
391
|
+
else:
|
|
392
|
+
# If db column and query column does not matches -> should be moved to validate_input
|
|
393
|
+
if not (gene in V.list_db_gene):
|
|
394
|
+
logging.error(
|
|
395
|
+
f"Gene {gene} found in query, but not found in database. Please add {gene} column to database"
|
|
396
|
+
)
|
|
397
|
+
raise Exception
|
|
398
|
+
|
|
399
|
+
# BLAST or mmseqs search
|
|
400
|
+
# This part should be changed by using former search result for faster performance
|
|
401
|
+
# No because changing database can result different bitscore, so each gene blast must be re-analyzed
|
|
402
|
+
for gene in V.list_qr_gene:
|
|
403
|
+
df_search = search(
|
|
404
|
+
query_fasta=f"{path.tmp}/{opt.runname}_Query_{gene}.fasta",
|
|
405
|
+
db_fasta=f"{path.tmp}/{opt.runname}_DB_{gene}.fasta",
|
|
406
|
+
path=path,
|
|
407
|
+
opt=opt,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# append group column
|
|
411
|
+
df_search["subject_group"] = df_search["sseqid"].apply(
|
|
412
|
+
lambda x: group_dict.get(x)
|
|
413
|
+
)
|
|
414
|
+
V.dict_gene_SR[gene] = df_search
|
|
415
|
+
|
|
416
|
+
# Save dataframe
|
|
417
|
+
if opt.nosearchresult is False:
|
|
418
|
+
save_df(
|
|
419
|
+
hasher.decode_df(V.dict_id_hash, df_search),
|
|
420
|
+
f"{path.out_matrix}/{opt.runname}_BLAST_result_{gene}.{opt.tableformat}",
|
|
421
|
+
opt.tableformat,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# remove tmp file after search
|
|
425
|
+
os.remove(f"{path.tmp}/{opt.runname}_Query_unclassified.fasta")
|
|
426
|
+
|
|
427
|
+
# remove temporary files
|
|
428
|
+
"""
|
|
429
|
+
for gene in opt.gene:
|
|
430
|
+
try:
|
|
431
|
+
os.remove(f"{path.tmp}/{opt.runname}_Query_{gene}.fasta")
|
|
432
|
+
except:
|
|
433
|
+
pass
|
|
434
|
+
try:
|
|
435
|
+
os.remove(f"{path.tmp}/{opt.runname}_DB_{gene}.fasta")
|
|
436
|
+
except:
|
|
437
|
+
pass
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
return V
|