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/concatenate.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from Bio import SeqIO
|
|
2
|
+
from Bio.SeqRecord import SeqRecord
|
|
3
|
+
from Bio.Seq import Seq
|
|
4
|
+
import itertools
|
|
5
|
+
import logging
|
|
6
|
+
from functools import reduce
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from funvip.src import search, hasher
|
|
10
|
+
from scipy.optimize import minimize
|
|
11
|
+
import numpy as np
|
|
12
|
+
import shutil
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Combine trimmed alignment of each gene to make concatenated matrix, and generate partition file
|
|
16
|
+
def combine_alignment(V, opt, path):
|
|
17
|
+
for group in V.dict_dataset:
|
|
18
|
+
if "concatenated" in V.dict_dataset[group]:
|
|
19
|
+
# If more than one locus exists
|
|
20
|
+
if len(V.dict_dataset[group]) > 2:
|
|
21
|
+
# get alignment length
|
|
22
|
+
# length of each of the alignments
|
|
23
|
+
len_dict = {}
|
|
24
|
+
seq_dict = {}
|
|
25
|
+
hash_set = set()
|
|
26
|
+
gene_list = []
|
|
27
|
+
|
|
28
|
+
for gene in V.dict_dataset[group]:
|
|
29
|
+
if not (gene == "concatenated"):
|
|
30
|
+
gene_list.append(gene)
|
|
31
|
+
|
|
32
|
+
fasta_list = list(
|
|
33
|
+
SeqIO.parse(
|
|
34
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{gene}.fasta",
|
|
35
|
+
"fasta",
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
len_dict[gene] = len(fasta_list[0].seq)
|
|
39
|
+
seq_dict[gene] = {}
|
|
40
|
+
total_dataset = (
|
|
41
|
+
[
|
|
42
|
+
FI.hash
|
|
43
|
+
for FI in V.dict_dataset[group][
|
|
44
|
+
"concatenated"
|
|
45
|
+
].list_qr_FI
|
|
46
|
+
]
|
|
47
|
+
+ [
|
|
48
|
+
FI.hash
|
|
49
|
+
for FI in V.dict_dataset[group][
|
|
50
|
+
"concatenated"
|
|
51
|
+
].list_db_FI
|
|
52
|
+
]
|
|
53
|
+
+ [
|
|
54
|
+
FI.hash
|
|
55
|
+
for FI in V.dict_dataset[group][
|
|
56
|
+
"concatenated"
|
|
57
|
+
].list_og_FI
|
|
58
|
+
]
|
|
59
|
+
)
|
|
60
|
+
for seq in fasta_list:
|
|
61
|
+
# if available hash
|
|
62
|
+
if seq.description in total_dataset:
|
|
63
|
+
seq_dict[gene][seq.description] = seq
|
|
64
|
+
hash_set.add(seq.description)
|
|
65
|
+
|
|
66
|
+
# Save partition information
|
|
67
|
+
V.partition[group] = {"len": len_dict, "order": gene_list}
|
|
68
|
+
|
|
69
|
+
# Generate partition file
|
|
70
|
+
with open(
|
|
71
|
+
f"{path.out_alignment}/{opt.runname}_{group}.partition", "w"
|
|
72
|
+
) as fw:
|
|
73
|
+
tot_len = 0
|
|
74
|
+
print(gene_list)
|
|
75
|
+
for gene in gene_list:
|
|
76
|
+
# gene_list.append(gene)
|
|
77
|
+
fw.write(f"DNA, {gene}= {tot_len+1}-{tot_len+len_dict[gene]}\n")
|
|
78
|
+
tot_len += len_dict[gene]
|
|
79
|
+
|
|
80
|
+
# Generate concatenated alignment file
|
|
81
|
+
concatenate_list = []
|
|
82
|
+
for hash_id in hash_set:
|
|
83
|
+
tmp_seq = ""
|
|
84
|
+
for gene in gene_list:
|
|
85
|
+
if hash_id in seq_dict[gene]:
|
|
86
|
+
tmp_seq += str(seq_dict[gene][hash_id].seq)
|
|
87
|
+
else:
|
|
88
|
+
# add gaps for ids without gene
|
|
89
|
+
tmp_seq += "-" * len_dict[gene]
|
|
90
|
+
|
|
91
|
+
concatenate_list.append(
|
|
92
|
+
SeqRecord(id=hash_id, description="", seq=Seq(tmp_seq))
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
SeqIO.write(
|
|
96
|
+
concatenate_list,
|
|
97
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_concatenated.fasta",
|
|
98
|
+
"fasta",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# For single gene, use same alignment from dataset of corresponding gene
|
|
102
|
+
elif len(V.dict_dataset[group]) == 2:
|
|
103
|
+
singlegene = list(set(V.dict_dataset[group].keys()) - {"concatenated"})[
|
|
104
|
+
0
|
|
105
|
+
]
|
|
106
|
+
# Copy single gene alignment
|
|
107
|
+
shutil.copy(
|
|
108
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{singlegene}.fasta",
|
|
109
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_concatenated.fasta",
|
|
110
|
+
)
|
|
111
|
+
# Generate partition file
|
|
112
|
+
gene_length = len(
|
|
113
|
+
str(
|
|
114
|
+
list(
|
|
115
|
+
SeqIO.parse(
|
|
116
|
+
f"{path.out_alignment}/{opt.runname}_trimmed_{group}_{singlegene}.fasta",
|
|
117
|
+
"fasta",
|
|
118
|
+
)
|
|
119
|
+
)[0].seq
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
with open(
|
|
123
|
+
f"{path.out_alignment}/{opt.runname}_{group}.partition", "w"
|
|
124
|
+
) as fw:
|
|
125
|
+
fw.write(f"DNA, {singlegene}= 1-{gene_length}\n")
|
|
126
|
+
|
|
127
|
+
# Save partition information
|
|
128
|
+
V.partition[group] = {
|
|
129
|
+
"len": {singlegene: gene_length},
|
|
130
|
+
"order": [singlegene],
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
else:
|
|
134
|
+
logging.error(f"V.dict_dataset {group}: {V.dict_dataset[group]}")
|
|
135
|
+
logging.error(
|
|
136
|
+
f"[DEVELOPMENTAL ERROR] Failed constructing concatenated alignment for {group}"
|
|
137
|
+
)
|
|
138
|
+
raise Exception
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
logging.warning(
|
|
142
|
+
f"For {group}, no more than genes were detected. Passing combining alignment"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return V
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# for concatenating blast result to get single bitscore among results
|
|
149
|
+
def concatenate_df(V, path, opt):
|
|
150
|
+
logging.info("Concatenating search results")
|
|
151
|
+
|
|
152
|
+
gene_list = []
|
|
153
|
+
df_list = []
|
|
154
|
+
for gene in V.dict_gene_SR.keys():
|
|
155
|
+
# Leave non-empty dataframes
|
|
156
|
+
if isinstance(V.dict_gene_SR[gene], pd.DataFrame):
|
|
157
|
+
gene_list.append(gene)
|
|
158
|
+
df = deepcopy(V.dict_gene_SR[gene].set_index(["qseqid", "sseqid"]))
|
|
159
|
+
df_list.append(df)
|
|
160
|
+
|
|
161
|
+
if len(df_list) <= 0:
|
|
162
|
+
logging.warning(f"Stop concatenating because same or less than 0 gene exists")
|
|
163
|
+
return V
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
# For multigene analysis, some genes can be missing
|
|
167
|
+
# Directly adding those values can make troubles if some of the genes are missing
|
|
168
|
+
# Therefore, add predicted values to fill them with linear regression
|
|
169
|
+
# drop unused columns
|
|
170
|
+
cnt = 0
|
|
171
|
+
|
|
172
|
+
# Change biscore names to delimitate genes
|
|
173
|
+
for n, df in enumerate(df_list):
|
|
174
|
+
gene = gene_list[n]
|
|
175
|
+
df[f"{gene}_bitscore"] = df["bitscore"]
|
|
176
|
+
df[f"{gene}_subject_group"] = df["subject_group"]
|
|
177
|
+
|
|
178
|
+
# Concatenate dataframes from multiple genes
|
|
179
|
+
df_multigene_regression_ori = pd.concat(df_list, axis=1)
|
|
180
|
+
|
|
181
|
+
# Drop unnecessary columns for processing
|
|
182
|
+
df_multigene_regression_ori.drop(
|
|
183
|
+
columns=[
|
|
184
|
+
"pident",
|
|
185
|
+
"length",
|
|
186
|
+
"mismatch",
|
|
187
|
+
"gaps",
|
|
188
|
+
"qstart",
|
|
189
|
+
"qend",
|
|
190
|
+
"sstart",
|
|
191
|
+
"send",
|
|
192
|
+
"evalue",
|
|
193
|
+
"bitscore",
|
|
194
|
+
"subject_group",
|
|
195
|
+
],
|
|
196
|
+
inplace=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Column name managing on subject_group
|
|
200
|
+
def same_merge(x, list_col):
|
|
201
|
+
values = x[list_col].dropna()
|
|
202
|
+
if values.empty:
|
|
203
|
+
raise Exception
|
|
204
|
+
return values.iloc[0]
|
|
205
|
+
|
|
206
|
+
df_multigene_regression_ori[
|
|
207
|
+
"subject_group"
|
|
208
|
+
] = df_multigene_regression_ori.apply(
|
|
209
|
+
lambda x: same_merge(x, [f"{gene}_subject_group" for gene in gene_list]),
|
|
210
|
+
axis=1,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# For regression, leave anchor points with all genes existing
|
|
214
|
+
df_multigene_regression = df_multigene_regression_ori
|
|
215
|
+
|
|
216
|
+
for gene in gene_list:
|
|
217
|
+
df_multigene_regression = df_multigene_regression[
|
|
218
|
+
df_multigene_regression[f"{gene}_bitscore"].notna()
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
# Perform regression
|
|
222
|
+
# Get regression line
|
|
223
|
+
def distance_to_line(line, pts, l0=None, p0=None):
|
|
224
|
+
"""
|
|
225
|
+
In three genes situation
|
|
226
|
+
(C0 - X0) / K0 = (C1 -X1) / K1 = (C2 - X2) / K2 = K
|
|
227
|
+
|
|
228
|
+
line = (C0, C1, C2)
|
|
229
|
+
pts = [(X0, X1, X2), (Y0, Y1, Y2) ... ]
|
|
230
|
+
p0 = (K0, K1, K2)
|
|
231
|
+
|
|
232
|
+
line defined between l0 and line
|
|
233
|
+
points defined between p0 and pts
|
|
234
|
+
|
|
235
|
+
This function calcuates following vector distance
|
|
236
|
+
D = (P - (P.dot.u) * u).length
|
|
237
|
+
"""
|
|
238
|
+
# line origin other than (0,0,0,..)
|
|
239
|
+
if l0 is not None:
|
|
240
|
+
line = line - l0
|
|
241
|
+
# points origin other than (0,0,0,..)
|
|
242
|
+
if p0 is not None:
|
|
243
|
+
pts = pts - p0
|
|
244
|
+
|
|
245
|
+
# dot product
|
|
246
|
+
dp = np.dot(pts, line)
|
|
247
|
+
# dot product value divided by normalized vector of line
|
|
248
|
+
# np.linalg.norm(line) : size of the line vector
|
|
249
|
+
# pp should be orthographic projected length of the dot
|
|
250
|
+
pp = dp / np.linalg.norm(line)
|
|
251
|
+
# norm value of point
|
|
252
|
+
# length from p0 to point
|
|
253
|
+
pn = np.linalg.norm(pts, axis=1)
|
|
254
|
+
|
|
255
|
+
return np.sqrt(np.clip(pn**2 - pp**2, a_min=1e-10, a_max=None))
|
|
256
|
+
|
|
257
|
+
# Optimization function
|
|
258
|
+
def optimize_regression_line(points):
|
|
259
|
+
n = points.shape[1] # Dimensionality of the points
|
|
260
|
+
|
|
261
|
+
# Define the objective function to minimize (R-squared)
|
|
262
|
+
def objective(x):
|
|
263
|
+
K = x[:n]
|
|
264
|
+
C = x[n:]
|
|
265
|
+
distances = distance_to_line(p0=C, line=K, pts=points)
|
|
266
|
+
mean_squared = np.mean(distances**2)
|
|
267
|
+
logging.debug(f"Mean_squared: {mean_squared}")
|
|
268
|
+
return mean_squared
|
|
269
|
+
|
|
270
|
+
# Initial guess for C and K values
|
|
271
|
+
# If C and K are same, it causes initialization error
|
|
272
|
+
x0 = np.full(2 * n, 1)
|
|
273
|
+
x0[:n] = 1
|
|
274
|
+
|
|
275
|
+
result = minimize(objective, x0)
|
|
276
|
+
|
|
277
|
+
# Extract the optimized C and K values
|
|
278
|
+
C_optimized = result.x[n:]
|
|
279
|
+
K_optimized = result.x[:n]
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
C_optimized,
|
|
283
|
+
K_optimized,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Reset df before filling it
|
|
287
|
+
def calculate_prediction(row, gene_list, coeff, grad):
|
|
288
|
+
# Calculate linear_constant of the strain
|
|
289
|
+
linear_constant = []
|
|
290
|
+
for k, gene in enumerate(gene_list):
|
|
291
|
+
if not np.isnan(row[f"{gene}_bitscore"]):
|
|
292
|
+
# (coeff - value) / gradient = linear constant
|
|
293
|
+
linear_constant.append(
|
|
294
|
+
(coeff[k] - row[f"{gene}_bitscore"]) / grad[k]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Predict unknown BLAST value
|
|
298
|
+
for k, gene in enumerate(gene_list):
|
|
299
|
+
if np.isnan(row[f"{gene}_bitscore"]):
|
|
300
|
+
# prediction value = coeff - linear constant * gradient
|
|
301
|
+
prediction = coeff[k] - np.mean(linear_constant) * grad[k]
|
|
302
|
+
row[f"{gene}_bitscore"] = prediction
|
|
303
|
+
return row
|
|
304
|
+
|
|
305
|
+
def apply_prediction(row, gene_list, coeff, grad):
|
|
306
|
+
row = calculate_prediction(row, gene_list, coeff, grad)
|
|
307
|
+
return row
|
|
308
|
+
|
|
309
|
+
# Change to numpy for faster cazlculation
|
|
310
|
+
np_bitscore = df_multigene_regression[
|
|
311
|
+
[f"{gene}_bitscore" for gene in gene_list]
|
|
312
|
+
].to_numpy()
|
|
313
|
+
|
|
314
|
+
# get coefficient and gradient with regression
|
|
315
|
+
coeff, grad = optimize_regression_line(np_bitscore)
|
|
316
|
+
|
|
317
|
+
# Inform users about linear regression result
|
|
318
|
+
# (C0 - X0) / K0 = (C1 -X1) / X1 = (C2 - X2) / X2 = K
|
|
319
|
+
trend_line_string = ""
|
|
320
|
+
for n, gene in enumerate(gene_list):
|
|
321
|
+
trend_line_string += f" {coeff[n]} - {gene} / {grad[n]} ="
|
|
322
|
+
|
|
323
|
+
trend_line_string += " K"
|
|
324
|
+
|
|
325
|
+
logging.info(
|
|
326
|
+
f"Trend line for Linear regression, \n {trend_line_string} \n Calculated to fill blank genes"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# fill empty blast results for each gene with regression
|
|
330
|
+
# This might be accelerated by using "apply", but coded manually initially because of logical complexity
|
|
331
|
+
df_multigene_regression = df_multigene_regression_ori.copy()
|
|
332
|
+
df_multigene_regression = df_multigene_regression.apply(
|
|
333
|
+
apply_prediction, args=(gene_list, coeff, grad), axis=1
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Also update each gene bitscore matrix
|
|
337
|
+
# This part is needed, for multigene analysis, for example ITS, CaM and RPB2
|
|
338
|
+
# If query only exists for ITS and CaM, no blast result for RPB2 were generated
|
|
339
|
+
# So we need to fill it out with linear regression
|
|
340
|
+
|
|
341
|
+
# Get summation and save to concatenated search result
|
|
342
|
+
df_multigene_regression["bitscore"] = df_multigene_regression[
|
|
343
|
+
[f"{gene}_bitscore" for gene in gene_list]
|
|
344
|
+
].mean(axis=1)
|
|
345
|
+
V.cSR = df_multigene_regression.reset_index()
|
|
346
|
+
|
|
347
|
+
# Save it
|
|
348
|
+
# decode df is not working well here
|
|
349
|
+
if opt.nosearchresult is False:
|
|
350
|
+
search.save_df(
|
|
351
|
+
hasher.decode_df(hash_dict=V.dict_id_hash, df=V.cSR),
|
|
352
|
+
f"{path.out_matrix}/{opt.runname}_BLAST_result_concatenated.{opt.tableformat}",
|
|
353
|
+
fmt=opt.tableformat,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return V
|