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/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