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/reporter.py ADDED
@@ -0,0 +1,875 @@
1
+ import datetime
2
+ import pandas as pd
3
+ import matplotlib.pyplot as plt
4
+ import io
5
+ import logging
6
+ import plotly.express as px
7
+ import sys
8
+ from tabulate import tabulate
9
+ from funvip.src.tool import index_step
10
+ from funvip.src.save import save_df
11
+
12
+
13
+ ### Temporary report for tree_interpretation_pipe
14
+ # To collect result from multiprocessing on tree_interpretation
15
+ # All abnormalities should be listed here
16
+ class Singlereport:
17
+ def __init__(self):
18
+ # FI info
19
+ self.id = ""
20
+ self.hash = ""
21
+ self.group = ""
22
+ self.gene = ""
23
+ self.species_original = ""
24
+ self.species_assigned = ""
25
+
26
+ # Analysis info
27
+ self.group_analysis = ""
28
+
29
+ # abnormalities
30
+ # for if ambiguous clade exists e.g. Amanita subglobosa "1", Amanita subglobosa "2"
31
+ # 0 if none of them exists
32
+ self.ambiguous = ""
33
+ self.flat = False
34
+
35
+ def update_group_analysis(self, group):
36
+ self.group_analysis = group
37
+
38
+ def update_group(self, group):
39
+ self.group = group
40
+
41
+ def update_gene(self, gene):
42
+ self.gene = gene
43
+
44
+ # Update original species string from (genus, species) tuple
45
+ def update_species_original(self, taxon):
46
+ self.species_original = " ".join(list(taxon))
47
+
48
+ # Update assigned species string
49
+ def update_species_assigned(self, taxon_str):
50
+ self.species_assigned = taxon_str
51
+
52
+ def __repr__(self):
53
+ return f"{self.hash} {self.gene}"
54
+
55
+
56
+ #############################################################
57
+
58
+
59
+ #############################################################
60
+ ### Final reporting data
61
+ ### All reports should be generated from here
62
+ class Report:
63
+ # Try to make report in dictionary form, most of them should be printed in tabular form
64
+ def __init__(self):
65
+ # Should be finished after release
66
+ # self.metadata = Metadata()
67
+
68
+ # Dataset - group by gene
69
+ # Example)
70
+ # GROUP ITS BenA CaM Concatenated
71
+ # Penicillium O O Fail outgroup O
72
+ # Exilicaluis O no query O O
73
+ # Whether to use just simple O/X or detailed description is not determined
74
+
75
+ self.dataset = {"GROUP": []}
76
+
77
+ # Identification result statistics : how many of them were well identified
78
+ # Example)
79
+ #
80
+
81
+ self.statistics = {
82
+ "GROUP": [],
83
+ "IDENTIFIED": [],
84
+ "NEW SPECIES CANDIDATE": [],
85
+ "MISIDENTIFIED": [],
86
+ "AMBIGUOUS": [],
87
+ "ERROR": [],
88
+ "TOTAL": [],
89
+ }
90
+
91
+ # For linking in html, should be added later (on GUI)
92
+ # self.statistics_link = {}
93
+
94
+ self.result = {
95
+ "ID": [],
96
+ "HASH": [],
97
+ "DATATYPE": [],
98
+ "ISSUES": [],
99
+ "GROUP_ORIGINAL": [],
100
+ "GROUP_ASSIGNED": [],
101
+ "SPECIES_ORIGINAL": [],
102
+ "SPECIES_ASSIGNED": [],
103
+ "STATUS": [],
104
+ }
105
+
106
+ self.query_result = None
107
+
108
+ def update_dataset(self, V, opt):
109
+ # Refresh when updating dataset
110
+ self.dataset = {"GROUP": []}
111
+
112
+ for gene in V.list_qr_gene:
113
+ self.dataset[gene.upper()] = []
114
+
115
+ self.dataset["CONCATENATE"] = []
116
+
117
+ for group in V.list_group:
118
+ self.dataset["GROUP"].append(group)
119
+ if group in V.dict_dataset:
120
+ for gene in V.list_qr_gene:
121
+ # if group - gene dataset exists
122
+ if gene in V.dict_dataset[group]:
123
+ self.dataset[gene.upper()].append("O")
124
+ # if gene exists but group does not exists
125
+ else:
126
+ self.dataset[gene.upper()].append("-")
127
+
128
+ if "concatenated" in V.dict_dataset[group]:
129
+ self.dataset["CONCATENATE"].append("O")
130
+ else:
131
+ self.dataset["CONCATENATE"].append("-")
132
+
133
+ else:
134
+ # If group does not exists
135
+ for gene in V.list_qr_gene:
136
+ self.dataset[gene.upper()].append("-")
137
+
138
+ self.dataset["CONCATENATE"].append("-")
139
+
140
+ # Combining result into dictionary form
141
+ def update_result(self, V, opt):
142
+ # get all gene list -> check if V.list_qr_gene is enough for queryonly : False analysis
143
+ set_gene = set(V.list_db_gene + V.list_qr_gene)
144
+
145
+ # Generate each gene identification report
146
+ for gene in sorted(list(set_gene)):
147
+ if gene != "concatenated":
148
+ self.result[f"{gene.upper()}_ASSIGNED"] = []
149
+
150
+ # Concatenated analysis will be operated seperately
151
+ # set_gene.discard("concatenated") try if without discarding works
152
+
153
+ ## Collect result from each hash
154
+ # Reset self.result to prevent continue mode stacking result double for some columns
155
+
156
+ self.result["ID"] = []
157
+ self.result["HASH"] = []
158
+ self.result["DATATYPE"] = []
159
+ self.result["ISSUES"] = []
160
+ self.result["GROUP_ORIGINAL"] = []
161
+ self.result["GROUP_ASSIGNED"] = []
162
+ self.result["SPECIES_ORIGINAL"] = []
163
+ self.result["SPECIES_ASSIGNED"] = []
164
+ self.result["STATUS"] = []
165
+
166
+ # Make FI categorizing system
167
+ FI_category = {}
168
+ dict_og = {}
169
+ for group in V.dict_dataset:
170
+ for gene in V.dict_dataset[group]:
171
+ for FI in V.dict_dataset[group][gene].list_og_FI:
172
+ dict_og[FI.hash] = group
173
+
174
+ for _hash in V.dict_hash_FI:
175
+ FI = V.dict_hash_FI[_hash]
176
+ if FI.adjusted_group in V.dict_dataset:
177
+ FI_category[_hash] = FI.datatype
178
+ elif FI.hash in dict_og:
179
+ FI_category[FI.hash] = "outgroup"
180
+
181
+ for _hash in V.dict_hash_FI:
182
+ FI = V.dict_hash_FI[_hash]
183
+
184
+ # Common results
185
+ self.result["ID"].append(FI.original_id)
186
+ self.result["HASH"].append(FI.hash)
187
+ if str(FI.group).strip() == "":
188
+ self.result["GROUP_ORIGINAL"].append("-")
189
+ else:
190
+ self.result["GROUP_ORIGINAL"].append(FI.group)
191
+
192
+ if f"{FI.ori_genus} {FI.ori_species}".strip() == "":
193
+ self.result["SPECIES_ORIGINAL"].append("-")
194
+ else:
195
+ self.result["SPECIES_ORIGINAL"].append(
196
+ f"{FI.ori_genus} {FI.ori_species}"
197
+ )
198
+ self.result["ISSUES"].append(FI.get_issue_str())
199
+
200
+ # Collect result from only used sequences
201
+ if _hash in FI_category:
202
+ self.result["DATATYPE"].append(FI_category[_hash])
203
+
204
+ # If any of appropriate gene used
205
+ if FI.adjusted_group in V.dict_dataset:
206
+ self.result["GROUP_ASSIGNED"].append(FI.adjusted_group)
207
+
208
+ # Collect assigned result
209
+ for gene in set_gene:
210
+ # Check if data analysis had performed for specific FI, group, gene combination
211
+ if (
212
+ gene in FI.bygene_species
213
+ and len(FI.seq[gene]) > 0
214
+ and gene in V.dict_dataset[FI.adjusted_group]
215
+ ):
216
+ self.result[f"{gene.upper()}_ASSIGNED"].append(
217
+ FI.bygene_species[gene]
218
+ )
219
+
220
+ else:
221
+ self.result[f"{gene.upper()}_ASSIGNED"].append("-")
222
+
223
+ # Add final identification result
224
+ if FI.final_species != "":
225
+ self.result["SPECIES_ASSIGNED"].append(f"{FI.final_species}")
226
+ else:
227
+ self.result["SPECIES_ASSIGNED"].append("UNDETERMINED")
228
+
229
+ # Should fix here
230
+ if FI_category[_hash] == "query":
231
+ if FI.final_species.strip() == "":
232
+ self.result["STATUS"].append("failed")
233
+ elif f"{FI.ori_genus} {FI.ori_species}".strip() == "":
234
+ if "sp." in FI.final_species.strip():
235
+ self.result["STATUS"].append("new species")
236
+ else:
237
+ self.result["STATUS"].append("assigned")
238
+
239
+ elif FI.final_species == f"{FI.ori_genus} {FI.ori_species}":
240
+ self.result["STATUS"].append("match")
241
+
242
+ elif FI.final_species != f"{FI.ori_genus} {FI.ori_species}":
243
+ self.result["STATUS"].append("conflict")
244
+ else:
245
+ self.result["STATUS"].append("-")
246
+ # For outgroup or ambiguous db
247
+ else:
248
+ self.result["GROUP_ASSIGNED"].append("-")
249
+ # Collect assigned result
250
+ for gene in set_gene:
251
+ self.result[f"{gene.upper()}_ASSIGNED"].append("-")
252
+ if FI.final_species != "":
253
+ self.result["SPECIES_ASSIGNED"].append(f"{FI.final_species}")
254
+ else:
255
+ self.result["SPECIES_ASSIGNED"].append("UNDETERMINED")
256
+ self.result["STATUS"].append("-")
257
+
258
+ # For unused FI
259
+ else:
260
+ self.result["DATATYPE"].append("unused")
261
+
262
+ for gene in set_gene:
263
+ # Check if data analysis had performed for specific FI, group, gene combination
264
+ self.result[f"{gene.upper()}_ASSIGNED"].append("-")
265
+
266
+ # Add final identification result
267
+ if FI.final_species != "":
268
+ self.result["SPECIES_ASSIGNED"].append(f"{FI.final_species}")
269
+ else:
270
+ self.result["SPECIES_ASSIGNED"].append("-")
271
+
272
+ self.result["STATUS"].append("unused")
273
+
274
+ ## update query only result on report.txt
275
+ self.query_result = pd.DataFrame(self.result)
276
+ # Filter if queryonly is True
277
+ if opt.queryonly is True:
278
+ self.query_result = self.query_result[
279
+ self.query_result["DATATYPE"] == "query"
280
+ ]
281
+
282
+ ### Update statistics by result
283
+
284
+ # Groupby group
285
+ df_result_group = self.query_result.groupby(["GROUP_ASSIGNED"])
286
+
287
+ # Count groups
288
+ for group in sorted(list(set(self.query_result["GROUP_ASSIGNED"]))):
289
+ df_group = df_result_group.get_group(group)
290
+
291
+ # Collect statistics
292
+ """
293
+ cnt_correctly_identified = list(df_group["STATUS"]).count(
294
+ "correctly identified"
295
+ )
296
+ cnt_undetermined = list(df_group["STATUS"]).count("undetermined")
297
+ cnt_new_species_candidate = list(df_group["STATUS"]).count(
298
+ "new species candidate"
299
+ )
300
+ cnt_misidentified = list(df_group["STATUS"]).count("misidentified")
301
+ cnt_error = list(df_group["STATUS"]).count("ERROR")
302
+ cnt_total = sum(
303
+ (
304
+ cnt_correctly_identified,
305
+ cnt_undetermined,
306
+ cnt_new_species_candidate,
307
+ cnt_misidentified,
308
+ cnt_error,
309
+ )
310
+ )
311
+ """
312
+
313
+ # Write into dictionary
314
+ """
315
+ self.statistics["GROUP"].append(group)
316
+ self.statistics["IDENTIFIED"].append(cnt_correctly_identified)
317
+ self.statistics["AMBIGUOUS"].append(cnt_undetermined)
318
+ self.statistics["NEW SPECIES CANDIDATE"].append(cnt_new_species_candidate)
319
+ self.statistics["MISIDENTIFIED"].append(cnt_misidentified)
320
+ self.statistics["ERROR"].append(cnt_error)
321
+ self.statistics["TOTAL"].append(cnt_total)
322
+ """
323
+
324
+ # Add final summations
325
+ """
326
+ self.statistics["GROUP"].append("TOTAL")
327
+ self.statistics["IDENTIFIED"].append(sum(self.statistics["IDENTIFIED"]))
328
+ self.statistics["AMBIGUOUS"].append(sum(self.statistics["AMBIGUOUS"]))
329
+ self.statistics["NEW SPECIES CANDIDATE"].append(
330
+ sum(self.statistics["NEW SPECIES CANDIDATE"])
331
+ )
332
+ self.statistics["MISIDENTIFIED"].append(sum(self.statistics["MISIDENTIFIED"]))
333
+ self.statistics["ERROR"].append(sum(self.statistics["ERROR"]))
334
+ self.statistics["TOTAL"].append(sum(self.statistics["TOTAL"]))
335
+ """
336
+
337
+ ### Main report runner
338
+ # Update report by pipeline step
339
+ def update_report(self, V, path, opt, step, version, GenMine_flag):
340
+ if step == "setup":
341
+ self.report_text(V, path, opt, step, version, GenMine_flag)
342
+ elif step == "search":
343
+ self.report_table(V, path, opt, step)
344
+ self.report_text(V, path, opt, step, version, GenMine_flag)
345
+ elif step == "cluster":
346
+ self.update_dataset(V, opt)
347
+ self.report_table(V, path, opt, step)
348
+ self.report_text(V, path, opt, step, version, GenMine_flag)
349
+ # Datasets are made after clustering
350
+ elif step == "align":
351
+ self.report_text(V, path, opt, step, version, GenMine_flag)
352
+ elif step == "trim":
353
+ self.report_text(V, path, opt, step, version, GenMine_flag)
354
+ elif step == "concatenate":
355
+ self.update_dataset(V, opt) # To check align fails
356
+ self.report_text(V, path, opt, step, version, GenMine_flag)
357
+ elif step == "modeltest":
358
+ self.report_text(V, path, opt, step, version, GenMine_flag)
359
+ elif step == "tree":
360
+ self.report_text(V, path, opt, step, version, GenMine_flag)
361
+ elif step == "visualize":
362
+ self.report_text(V, path, opt, step, version, GenMine_flag)
363
+ elif step == "report":
364
+ self.update_result(V, opt)
365
+ self.report_table(V, path, opt, step)
366
+ self.report_text(V, path, opt, step, version, GenMine_flag)
367
+ else:
368
+ logging.error(
369
+ f"DEVELOPMENTAL ERROR : BAD STEP INPUT {step} WHILE UPDATE REPORT"
370
+ )
371
+ raise Exception
372
+
373
+ # Generate report in text file
374
+ def report_text(self, V, path, opt, step, version, GenMine_flag):
375
+ # Rewrite everytime when called, the io step won't be that much
376
+ with open(f"{path.root}/{opt.runname}.report.txt", "wt", encoding="UTF8") as f:
377
+ if index_step(step) >= 0:
378
+ f.write(
379
+ "Visualization of this file is optimized to web browsers (such as chrome) or width unlimited text editors (such as visual studio code)\n\n"
380
+ )
381
+ f.write("FunVIP Report\n\n")
382
+ ## Write runinfo
383
+ f.write("[INFO]\n")
384
+ # Runname
385
+ f.write(f"RUNNAME : {opt.runname}\n")
386
+ # Running date
387
+ f.write(
388
+ f"DATE : {datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}\n"
389
+ )
390
+ # Running time
391
+ # f.write(f"TIME CONSUMED: {}\n")
392
+ # Number of warnings
393
+ # f.write(f"WARNINGS : {}\n")
394
+ # Number of errors
395
+ # f.write(f"ERRORS : {}\n")
396
+ if step == "report":
397
+ step_status = "pipeline succesfully finished"
398
+ else:
399
+ step_status = (
400
+ "pipeline unexpectly terminated - please check log file"
401
+ )
402
+
403
+ f.write(f"FINAL STEP : {step} ({step_status})\n")
404
+
405
+ f.write("\n")
406
+
407
+ ## Write locations to result files
408
+ if index_step(step) >= 0:
409
+ # DB and Query locations can be written after input step
410
+ f.write(f"* DB files are saved in {path.out_db}\n")
411
+ f.write(f"* Query files are saved in {path.out_query}\n")
412
+ # Options location can be written after option validation step
413
+ f.write(f"* Options are saved in {path.root}/Options.yaml\n")
414
+
415
+ if index_step(step) >= 2:
416
+ # Datset table can be written after cluster and outgroup selection step
417
+ f.write(
418
+ f"* Dataset table can be found in {path.root}/{opt.runname}_Dataset.{opt.tableformat}\n"
419
+ )
420
+ # Identification table can be written after cluster step
421
+ f.write(
422
+ f"* Identification result table can be found in {path.root}/{opt.runname}_Identification.{opt.tableformat}\n"
423
+ )
424
+ f.write(f"* Log files can be found in {path.log}\n")
425
+
426
+ if index_step(step) >= 8:
427
+ f.write(f"* Tree files can be found in {path.out_tree}\n")
428
+
429
+ f.write("\n")
430
+
431
+ ## Write full commands used
432
+ if index_step(step) >= 0:
433
+ f.write(f"[COMMAND]\n")
434
+ f.write(f"If you want to regenerate this result, use\n")
435
+ f.write(f"\n\tFUNVIP {' '.join(sys.argv[:-1])}\n\n")
436
+
437
+ ## Write options used (in concise form)
438
+ if index_step(step) >= 0:
439
+ f.write(f"[OPTION]\n")
440
+ f.write(f"DB: {opt.query}\n")
441
+ f.write(f"QUERY: {opt.db}\n")
442
+ f.write(f"GENE: {opt.gene}\n")
443
+ f.write(f"EMAIL: {opt.email}\n")
444
+ f.write(f"API: {opt.api}\n")
445
+ f.write(f"TEST: {opt.test}\n")
446
+ f.write(f"THREAD: {opt.thread}\n")
447
+ f.write(f"OUTDIR: {opt.outdir}\n")
448
+ f.write(f"RUNNAME: {opt.runname}\n")
449
+ f.write(f"MODE: {opt.mode}\n")
450
+ f.write(f"CONTINUE_FROM_PREVIOUS: {opt.continue_from_previous}\n")
451
+ f.write(f"CRITERION: {opt.criterion}\n")
452
+ f.write(f"STEP: {opt.step}\n")
453
+ f.write(f"LEVEL: {opt.level}\n")
454
+ f.write(f"QUERYONLY: {opt.queryonly}\n")
455
+ f.write(f"CONFIDENT: {opt.confident}\n")
456
+ f.write(f"VERBOSE: {opt.verbose}\n")
457
+ f.write(f"MAXOUTGROUP: {opt.maxoutgroup}\n")
458
+ f.write(f"COLLAPSEDISTCUTOFF: {opt.collapsedistcutoff}\n")
459
+ f.write(f"COLLAPSEBSCUTOFF: {opt.collapsebscutoff}\n")
460
+ f.write(f"BOOTSTRAP: {opt.bootstrap}\n")
461
+ f.write(f"SOLVEFLAT: {opt.solveflat}\n")
462
+ f.write(f"REGEX: {opt.regex}\n")
463
+ f.write(f"AVX: {opt.avx}\n")
464
+ f.write(f"CACHEDB: {opt.cachedb}\n")
465
+ f.write(f"USECACHE: {opt.usecache}\n")
466
+ f.write(f"TABLEFORMAT: {opt.tableformat}\n")
467
+ f.write(f"NOSEARCHRESULT: {opt.nosearchresult}\n")
468
+ f.write(f"METHOD: \n")
469
+ f.write(f" - SEARCH: {opt.method.search}\n")
470
+ f.write(f" - ALIGNMENT: {opt.method.alignment}\n")
471
+ f.write(f" - TRIM: {opt.method.trim}\n")
472
+ f.write(f" - MODELTEST: {opt.method.modeltest}\n")
473
+ f.write(f" - TREE: {opt.method.tree}\n")
474
+ f.write(f"VISUALIZE: \n")
475
+ f.write(f" - BSCUTOFF: {opt.visualize.bscutoff}\n")
476
+ f.write(f" - HIGHLIGHT: {opt.visualize.highlight}\n")
477
+ f.write(f" - HEIGHTMULTIPLIER: {opt.visualize.heightmultiplier}\n")
478
+ f.write(f" - MAXWORDLENGTH: {opt.visualize.maxwordlength}\n")
479
+ f.write(f" - BGCOlOR: {opt.visualize.backgroundcolor}\n")
480
+ f.write(f" - OUTGROUPCOLOR: {opt.visualize.outgroupcolor}\n")
481
+ f.write(f" - FTYPE: {opt.visualize.ftype}\n")
482
+ f.write(f" - FSIZE: {opt.visualize.fsize}\n")
483
+ f.write(f" - FSIZE_BOOTSTRAP: {opt.visualize.fsize_bootstrap}\n")
484
+ f.write(f"CLUSTER: \n")
485
+ f.write(f" - CUTOFF: {opt.cluster.cutoff}\n")
486
+ f.write(f" - EVALUE: {opt.cluster.evalue}\n")
487
+ f.write(f" - WORDSIZE: {opt.cluster.wordsize}\n")
488
+ f.write(f"MAFFT: \n")
489
+ f.write(f" - ALGORITHM: {opt.mafft.algorithm}\n")
490
+ f.write(f" - OP: {opt.mafft.op}\n")
491
+ f.write(f" - EP: {opt.mafft.ep}\n")
492
+ f.write(f"TRIMAL: \n")
493
+ f.write(f" - ALGORITHM: {opt.trimal.algorithm}\n")
494
+ f.write(f" - GT: {opt.trimal.gt}\n")
495
+ f.write("\n")
496
+
497
+ ## Write datasets
498
+ # Should be written after clustering
499
+ if index_step(step) >= 2:
500
+ f.write(f"[DATASET]\n")
501
+ f.write(tabulate(self.dataset, headers=self.dataset.keys()))
502
+ f.write("\n\n")
503
+ f.write("O : Group - Gene dataset analysis done\n")
504
+ f.write(
505
+ "- : Group - Gene dataset analysis cannot be performed (absence of query sequence, no appropriate outgroup, alignment fail etc..)\n"
506
+ )
507
+ f.write("\n\n")
508
+
509
+ ## Write identification statistics
510
+ # Should be written after tree_interpretation
511
+ '''
512
+ if index_step(step) >= 9:
513
+ f.write(f"[STATISTICS]\n")
514
+ f.write(tabulate(self.statistics, headers=self.statistics.keys()))
515
+ f.write("\n\n")
516
+ f.write(
517
+ "IDENTIFIED : Number of well-identified strains without any concerns. \n"
518
+ )
519
+ """
520
+ f.write(
521
+ "AMBIGUOUS : Multiple clades with same taxon name. Your database may contain misidentified sequences. \n"
522
+ )
523
+ """
524
+ f.write(
525
+ "NEW SPECIES CANDIDATE : New species candidate strains found by topology, phylogenetic distance and bootstrap criteria\n"
526
+ )
527
+ f.write(
528
+ "MISIDENTIFIED : Strains that shows different identification result from original annotation\n"
529
+ )
530
+ f.write(
531
+ "ERROR : Strains that cannot be analyzed. Please check if appropriate database sequence / outgroup sequences are given\n"
532
+ )
533
+ f.write("\n\n")
534
+ '''
535
+
536
+ ## Write identification result
537
+ # Should be written after tree_interpretation
538
+ if index_step(step) >= 9:
539
+ f.write(f"[IDENTIFICATION]\n")
540
+ if opt.queryonly is True:
541
+ f.write(
542
+ tabulate(
543
+ self.query_result,
544
+ headers=self.query_result.columns,
545
+ showindex=False,
546
+ )
547
+ )
548
+ else:
549
+ f.write(tabulate(self.result, headers=self.result.keys()))
550
+ f.write("\n\n")
551
+ f.write("ID : Name of the strain\n")
552
+ f.write(
553
+ "HASH : Temporary name of the strain to prevent unexpected error during run. Use this when manually edit intermediate step data and run from middle, or debugging unexpectively terminated run\n"
554
+ )
555
+ f.write("DATATYPE : query or database\n")
556
+ f.write("GROUP_ORIGINAL : group name given by user\n")
557
+ f.write("GROUP_ASSIGNED : group assigned by FunVIP clustering\n")
558
+ f.write("SPECIES_ORIGINAL : species name given by user\n")
559
+ f.write(
560
+ "SPECIES_ASSIGNED : final species name (usually result from concatenated analysis) assigned by FunVIP tree_interpretation\n"
561
+ )
562
+ f.write(
563
+ "FLAT_BRANCH : Strains with flat_branch in phylogenetic analysis. If checked, please check your barcode region have enough taxonomic resolution\n"
564
+ )
565
+ f.write(
566
+ "INCONSISTENT : Strains that show different identification result across genes. Please check sequences were contaminated or misused. \n"
567
+ )
568
+ """
569
+ f.write(
570
+ "AMBIGUOUS : Multiple clades with same taxon name. Your database may contain misidentified sequences. \n"
571
+ )
572
+ """
573
+
574
+ f.write("\n\n")
575
+
576
+ ## Write identification methods
577
+ # Can be written after clustering step
578
+ if index_step(step) >= 1:
579
+ f.write(f"[METHOD]\n")
580
+ f.write(f"Sequences were identified with FunVIP {version.FunVIP}\n")
581
+ f.write("\n")
582
+
583
+ cnt_db = len([FI for FI in V.list_FI if FI.datatype == "db"])
584
+ cnt_query = len([FI for FI in V.list_FI if FI.datatype == "query"])
585
+
586
+ f.write("- Sequence validation -\n")
587
+ f.write(
588
+ f"Total of {cnt_db} database strains and {cnt_query} strains "
589
+ f"were used for analysis. "
590
+ )
591
+
592
+ f.write(
593
+ f"Sequences used for analysis were first adjusted to prevent errors during analysis, "
594
+ f"such as containing invalid bases, non-ascii unicodes, or empty database.\n"
595
+ # f"During sequences validation step, {cnt_warning} warnings and {cnt_error} errors occured"
596
+ )
597
+ f.write("\n")
598
+ """
599
+ for warning in validate_input_warning:
600
+ f.write(f"\n")
601
+ for error in validate_input_error:
602
+ f.write(f"\n")
603
+ """
604
+ f.write(
605
+ "* Most of the warnings in this step are usually typo problems "
606
+ "(blanks, tabs, foreign languages that cannot be used in certain programs - like german umlauts) "
607
+ "and can be automatically fixed by FunVIP. So you don't have to consider about it that much if you are not going to directly publish.\n"
608
+ )
609
+ f.write("\n")
610
+
611
+ if index_step(step) >= 2:
612
+ f.write("- Sequence type identification -\n")
613
+ f.write(
614
+ f"Sequence type (loci or gene) of query sequences were examined through {opt.method.search} search "
615
+ f"by using gene of the closeset match. However, ambiguous match with multiple number of genes were revised. "
616
+ # f"During gene clustering, {cnt_warning} warnings and {cnt_error} errors occured "
617
+ )
618
+ f.write("\n")
619
+ """
620
+ for warning in gene_clustering_warning:
621
+ f.write(f"\n")
622
+ for error in gene_clustering_error:
623
+ f.write(f"\n")
624
+ """
625
+ f.write("\n")
626
+
627
+ f.write("- Group assignment -\n")
628
+ f.write(
629
+ f"Sequences were grouped to each datasets by {opt.level} level for phylogenetic analysis through {opt.method.search} search. "
630
+ f"Therefore, {opt.level} of query sequences are either assigned (when {opt.level} is not given) or validated for clustering "
631
+ # f"During clustering, {cnt_warning} warnings and {cnt_error} errors occured "
632
+ )
633
+ f.write("\n")
634
+ """
635
+ for warning in group_clustering_warning:
636
+ f.write(f"\n")
637
+ for error in group_clustering_error:
638
+ f.write(f"\n")
639
+ """
640
+ f.write("\n")
641
+
642
+ f.write("- Outgroup selection -\n")
643
+ f.write(
644
+ f"Outgroup sequences were appended to each datasets. "
645
+ f"{opt.maxoutgroup} sequences closest, but apperantly in distinct group were found by {opt.method.search} search. "
646
+ # f"During outgroup selection, {cnt_warning} warnings and {cnt_error} errors occured"
647
+ )
648
+ f.write("\n")
649
+ """
650
+ for warning in append_outgroup_warning:
651
+ f.write(f"\n")
652
+ for error in append_outgroup_error:
653
+ f.write(f"\n")
654
+ """
655
+ f.write("\n")
656
+
657
+ f.write("- Multiple sequence alignment -\n")
658
+ f.write(
659
+ f"Multiple sequence alignment to each datasets were performed with {opt.method.alignment}. "
660
+ f"{opt.mafft.algorithm} algorithm was selected with --op {opt.mafft.op} and --ep {opt.mafft.ep} options. "
661
+ # f"During alignment, {cnt_warning} warnings and {cnt_error} errors occured"
662
+ )
663
+ f.write("\n")
664
+ """
665
+ for warning in mafft_warning:
666
+ f.write(f"\n")
667
+ for error in mafft_error:
668
+ f.write(f"\n")
669
+ """
670
+ f.write("\n")
671
+
672
+ f.write("- Alignment trimming -\n")
673
+
674
+ if not opt.method.trim == "none":
675
+ f.write(
676
+ f"Trimming to each datasets were performed with {opt.method.trim}. "
677
+ # f"{somewhat} options were used"
678
+ # f"During trimming, {cnt_warning} warnings and {cnt_error} errors occured"
679
+ )
680
+ else:
681
+ f.write(f"No trimming performed on sequence alignments. ")
682
+ f.write("\n")
683
+ """
684
+ for warning in trim_warning:
685
+ f.write(f"\n")
686
+ for error in trim_error:
687
+ f.write(f"\n")
688
+ """
689
+ f.write("\n")
690
+
691
+ f.write("- Phylogenetic tree construction -\n")
692
+ f.write(
693
+ f"Maximum Likelihood tree analysis to each datasets were performed with {opt.method.tree}. "
694
+ # f"{somewhat} options were used"
695
+ # f"During trimming, {cnt_warning} warnings and {cnt_error} errors occured"
696
+ )
697
+ f.write("\n")
698
+ """
699
+ for warning in tree_warning:
700
+ f.write(f"\n")
701
+ for error in tree_error:
702
+ f.write(f"\n")
703
+ """
704
+ f.write("\n")
705
+
706
+ f.write("- Phylogenetic tree interpretation and identification -\n")
707
+ f.write(
708
+ f"Tree interpretation were performed to each datasets for species delimitation and tree visualization. "
709
+ f"Trees were rerooted by outgroup clades. "
710
+ f"Leaves in ambiguous tree topology, with over {opt.collapsedistcutoff} tree distance "
711
+ )
712
+
713
+ if opt.collapsebscutoff < 100:
714
+ f.write(
715
+ f"or over {opt.collapsebscutoff} support"
716
+ # f"During tree interpretation, {cnt_warning} warnings and {cnt_error} errors occured"
717
+ )
718
+
719
+ f.write(
720
+ f"in common ancestor diverge were considered as distinct species. "
721
+ )
722
+
723
+ f.write("\n")
724
+ """
725
+ for warning in tree_interpretation_warning:
726
+ f.write(f"\n")
727
+ for error in tree_interpretation_error:
728
+ f.write(f"\n")
729
+ """
730
+ f.write("\n")
731
+
732
+ """
733
+ f.write(
734
+ f"As a result of FunVIP analysis, a total of {cnt_query} strains were identified"
735
+ f"{cnt_query} sequences constists of"
736
+ f"{cnt_consistent} well identified strains,"
737
+ f"{cnt_ambiguous} strains that showed different results by genes,"
738
+ f"{cnt_new_species_candidates} new species candidates,"
739
+ f"{cnt_error} failed on analysis due to error"
740
+ f"During tree interpreation, {cnt_warning} warnings and {cnt_error} errors occured"
741
+ )
742
+ for warning in tree_interpretation_warning:
743
+ f.write(f"\n")
744
+ for error in tree_interpretation_error:
745
+ f.write(f"\n")
746
+ """
747
+
748
+ f.write("\n")
749
+ f.write(
750
+ f"* For precise identification and publications, please double check all warnings and errors\n"
751
+ )
752
+ f.write("\n")
753
+
754
+ # Generate software list by options
755
+ software_list = ["FunVIP"]
756
+ if GenMine_flag != 0:
757
+ software_list.append("GenMine")
758
+
759
+ if index_step(step) >= 1:
760
+ if opt.method.search == "blast":
761
+ software_list.append("BLASTn")
762
+ elif opt.method.search == "mmseqs":
763
+ software_list.append("MMseqs2")
764
+
765
+ if index_step(step) >= 3:
766
+ if opt.method.alignment == "mafft":
767
+ software_list.append("MAFFT")
768
+
769
+ if index_step(step) >= 4:
770
+ if opt.method.trim == "gblocks":
771
+ software_list.append("Gblocks")
772
+ elif opt.method.trim == "trimal":
773
+ software_list.append("TrimAl")
774
+
775
+ if index_step(step) >= 6:
776
+ if opt.method.modeltest == "modeltest-ng":
777
+ software_list.append("Modeltest-NG")
778
+ elif opt.method.modeltest == "iqtree":
779
+ software_list.append("Partitionfinder")
780
+
781
+ if index_step(step) >= 7:
782
+ if opt.method.tree == "fasttree":
783
+ software_list.append("FastTree")
784
+ elif opt.method.tree == "iqtree":
785
+ software_list.append("IQTREE2")
786
+ elif opt.method.tree == "raxml":
787
+ software_list.append("RAxML")
788
+
789
+ # append software list by step
790
+ # Should add GenMine version here
791
+ dict_version = {
792
+ "FunVIP": version.FunVIP,
793
+ "GenMine": version.GenMine,
794
+ "BLASTn": version.BLASTn,
795
+ "MMseqs2": version.MMseqs2,
796
+ "MAFFT": version.MAFFT,
797
+ "Gblocks": version.Gblocks,
798
+ "TrimAl": version.trimAl,
799
+ "Modeltest-NG": version.Modeltest_NG,
800
+ "Partitionfinder": version.IQTREE2,
801
+ "FastTree": version.FastTree,
802
+ "IQTREE2": version.IQTREE2,
803
+ "RAxML": version.RAxML,
804
+ }
805
+
806
+ ### Version for each software notation
807
+ f.write(f"[VERSIONS]\n")
808
+
809
+ for n, software in enumerate(software_list):
810
+ f.write(f"{'{:<15}'.format(software)} : {dict_version[software]}\n")
811
+
812
+ f.write(f"\n")
813
+
814
+ ### Write citations
815
+ f.write(f"[CITATION]\n")
816
+ dict_citation = {
817
+ "FunVIP": "https://github.com/Changwanseo/FunVIP",
818
+ "GenMine": "Seo, C. W., Kim, S. H., Lim, Y. W., & Park, M. S. (2022). Re-identification on Korean Penicillium sequences in GenBank collected by software GenMine. Mycobiology, 50(4), 231-237.",
819
+ "BLASTn": "Altschul, S. F., Gish, W., Miller, W., Myers, E. W., & Lipman, D. J. (1990). Basic local alignment search tool. Journal of molecular biology, 215(3), 403-410.",
820
+ "MMseqs2": "Steinegger, M., & Söding, J. (2017). MMseqs2 enables sensitive protein sequence searching for the analysis of massive data sets. Nature biotechnology, 35(11), 1026-1028.",
821
+ "MAFFT": "Katoh, K., & Standley, D. M. (2013). MAFFT multiple sequence alignment software version 7: improvements in performance and usability. Molecular biology and evolution, 30(4), 772-780.",
822
+ "Gblocks": "Talavera, G., & Castresana, J. (2007). Improvement of phylogenies after removing divergent and ambiguously aligned blocks from protein sequence alignments. Systematic biology, 56(4), 564-577.",
823
+ "TrimAl": "Capella-Gutiérrez, S., Silla-Martínez, J. M., & Gabaldón, T. (2009). trimAl: a tool for automated alignment trimming in large-scale phylogenetic analyses. Bioinformatics, 25(15), 1972-1973.",
824
+ "Modeltest-NG": "Darriba, D., Posada, D., Kozlov, A. M., Stamatakis, A., Morel, B., & Flouri, T. (2020). ModelTest-NG: a new and scalable tool for the selection of DNA and protein evolutionary models. Molecular biology and evolution, 37(1), 291-294.",
825
+ "Partitionfinder": "Lanfear, R., Frandsen, P. B., Wright, A. M., Senfeld, T., & Calcott, B. (2017). PartitionFinder 2: new methods for selecting partitioned models of evolution for molecular and morphological phylogenetic analyses. Molecular biology and evolution, 34(3), 772-773.",
826
+ "FastTree": "Price, M. N., Dehal, P. S., & Arkin, A. P. (2010). FastTree 2–approximately maximum-likelihood trees for large alignments. PloS one, 5(3), e9490.",
827
+ "IQTREE2": "Minh, B. Q., Schmidt, H. A., Chernomor, O., Schrempf, D., Woodhams, M. D., Von Haeseler, A., & Lanfear, R. (2020). IQ-TREE 2: new models and efficient methods for phylogenetic inference in the genomic era. Molecular biology and evolution, 37(5), 1530-1534.",
828
+ "RAxML": "Stamatakis, A. (2014). RAxML version 8: a tool for phylogenetic analysis and post-analysis of large phylogenies. Bioinformatics, 30(9), 1312-1313.",
829
+ }
830
+
831
+ for n, software in enumerate(software_list):
832
+ f.write(
833
+ f"[{n+1}] {'{:<15}'.format(software)} : {dict_citation[software]}\n"
834
+ )
835
+
836
+ def report_table(self, V, path, opt, step):
837
+ # Generate list of table to be written
838
+ list_table = []
839
+
840
+ if index_step(step) >= 2:
841
+ list_table.append("dataset")
842
+
843
+ if index_step(step) >= 9:
844
+ list_table.append("identification")
845
+ # list_table.append("statistics")
846
+
847
+ # Write tables
848
+ for table in list_table:
849
+ if table == "dataset":
850
+ save_df(
851
+ df=pd.DataFrame(self.dataset),
852
+ out=f"{path.root}/{opt.runname}.dataset.{opt.tableformat}",
853
+ fmt=opt.tableformat,
854
+ )
855
+ elif table == "identification":
856
+ save_df(
857
+ df=pd.DataFrame(self.result),
858
+ out=f"{path.root}/{opt.runname}.result.{opt.tableformat}",
859
+ fmt=opt.tableformat,
860
+ )
861
+
862
+ elif table == "statistics":
863
+ save_df(
864
+ df=pd.DataFrame(self.statistics),
865
+ out=f"{path.root}/{opt.runname}.statistics.{opt.tableformat}",
866
+ fmt=opt.tableformat,
867
+ )
868
+ else:
869
+ logging.error(
870
+ f"DEVELOPMENTAL ERROR : WRONG TABLE TYPE {table} ATTEMPTED TO BE WRITTEN"
871
+ )
872
+ raise Exception
873
+
874
+ def report_html(V, path, opt):
875
+ pass