Replidec 0.3.6__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.
- Replidec/Replidec.py +346 -0
- Replidec/Replidec_cmdline.py +118 -0
- Replidec/Replidec_multi.py +287 -0
- Replidec/__init__.py +18 -0
- Replidec/utility.py +36 -0
- replidec-0.3.6.dist-info/LICENSE +22 -0
- replidec-0.3.6.dist-info/METADATA +242 -0
- replidec-0.3.6.dist-info/RECORD +12 -0
- replidec-0.3.6.dist-info/WHEEL +5 -0
- replidec-0.3.6.dist-info/entry_points.txt +3 -0
- replidec-0.3.6.dist-info/top_level.txt +1 -0
- replidec-0.3.6.dist-info/zip-safe +1 -0
Replidec/Replidec.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# authors: sherry peng, torben sanders, erfan khamespanah
|
|
4
|
+
# mail: xue.peng@helmholtz-muenchen.de
|
|
5
|
+
# date: 2026.07.09
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
import os
|
|
11
|
+
from subprocess import Popen, PIPE
|
|
12
|
+
import time
|
|
13
|
+
from Replidec.utility import mkdirs, checkEnv
|
|
14
|
+
|
|
15
|
+
# Master remote references pointing to static versioned database checkpoints
|
|
16
|
+
DATABASE_MANIFEST = {
|
|
17
|
+
"version": "0.3.3",
|
|
18
|
+
"url": "https://zenodo.org/records/22178169/files/db_v0.3.3.tgz",
|
|
19
|
+
"integrase_hmm": "db/integrase_pfv34.hmm",
|
|
20
|
+
"excisionase_hmm": "db/excisionase_pfv34.hmm",
|
|
21
|
+
"mmseqs_index": "db/bayes_mmseqs_index/training_prot_04_2025",
|
|
22
|
+
"scoring_matrix": "db/prokaryote_only_training_cluster_04_2025.stat.scoreOpt.tsv",
|
|
23
|
+
"inovirus_hmm": "db/Final_marker_morph.hmm",
|
|
24
|
+
"inovirus_blast": "db/Marker_ALV1"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def _get_timestamp():
|
|
28
|
+
"""Generates standard tracking prefixes for log reporting."""
|
|
29
|
+
return time.strftime("[%Y-%m-%d %H:%M:%S]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def runProdigal(inputseq, prefix, wd, program="meta", otherPara="-g 11"):
|
|
33
|
+
"""
|
|
34
|
+
Runs Prodigal for gene calling.
|
|
35
|
+
Redirects stdout/stderr to an isolated log within the user's working directory.
|
|
36
|
+
"""
|
|
37
|
+
checkEnv("prodigal")
|
|
38
|
+
mkdirs(wd)
|
|
39
|
+
log_file = os.path.join(wd, "external_tools.log")
|
|
40
|
+
|
|
41
|
+
cmd = "prodigal -i {0} {4} -a {2}/{1}.prodigal.gene.faa -d {2}/{1}.prodigal.gene.ffn -p {3} -f gff -o {2}/{1}.temp >> {5} 2>&1".format(
|
|
42
|
+
inputseq, prefix, wd, program, otherPara, log_file
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
print(f"{_get_timestamp()} [INFO] Running Prodigal: {prefix}")
|
|
46
|
+
obj = Popen(cmd, shell=True)
|
|
47
|
+
obj.wait()
|
|
48
|
+
return os.path.join(wd, f"{prefix}.prodigal.gene.faa")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def runHmmsearch(inputfile, prefix, wd, hmmModel, otherPara="--noali --cpu 1"):
|
|
52
|
+
"""Executes target profile scans against reference hidden Markov databases."""
|
|
53
|
+
checkEnv("hmmsearch")
|
|
54
|
+
mkdirs(wd)
|
|
55
|
+
log_file = os.path.join(wd, "external_tools.log")
|
|
56
|
+
output_tbl = os.path.join(wd, f"{prefix}.hmmsearch.tblout")
|
|
57
|
+
output_out = os.path.join(wd, f"{prefix}.hmmsearch.out")
|
|
58
|
+
|
|
59
|
+
cmd = "hmmsearch {4} -o {5} --tblout {2} {3} {0} >> {1} 2>&1".format(
|
|
60
|
+
inputfile, log_file, output_tbl, hmmModel, otherPara, output_out
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(f"{_get_timestamp()} [INFO] Running HMMer search: {prefix}")
|
|
64
|
+
obj = Popen(cmd, shell=True)
|
|
65
|
+
obj.wait()
|
|
66
|
+
return output_tbl
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def runMmseqsEasysearch(inputfile, prefix, wd, hmmDB,
|
|
70
|
+
otherPara="-s 7 --max-seqs 1 --alignment-mode 3 --alignment-output-mode 0 "
|
|
71
|
+
"--min-aln-len 40 --cov-mode 0 --greedy-best-hits 1 --threads 30"):
|
|
72
|
+
"""Executes fast protein homology searches using MMseqs2."""
|
|
73
|
+
checkEnv("mmseqs")
|
|
74
|
+
mkdirs(wd)
|
|
75
|
+
log_file = os.path.join(wd, "external_tools.log")
|
|
76
|
+
|
|
77
|
+
tmpdir = os.path.join(wd, f"{prefix}_tmp")
|
|
78
|
+
mkdirs(tmpdir)
|
|
79
|
+
output = os.path.join(wd, f"{prefix}.mmseqs.m8")
|
|
80
|
+
|
|
81
|
+
cmd = "mmseqs easy-search {0} {1} {2} {3} {4} >> {5} 2>&1 && rm -rf {3}".format(
|
|
82
|
+
inputfile, hmmDB, output, tmpdir, otherPara, log_file
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
print(f"{_get_timestamp()} [INFO] Running MMseqs2 easy-search: {prefix}")
|
|
86
|
+
obj = Popen(cmd, shell=True)
|
|
87
|
+
obj.wait()
|
|
88
|
+
return output
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def runBlastpsearch(inputfile, prefix, wd, db_prefix, evalue="1e-3", otherPara="-num_threads 3"):
|
|
92
|
+
"""Standard BLASTp sequence alignment."""
|
|
93
|
+
checkEnv("blastp")
|
|
94
|
+
mkdirs(wd)
|
|
95
|
+
log_file = os.path.join(wd, "external_tools.log")
|
|
96
|
+
output = os.path.join(wd, f"{prefix}.blastp.m8")
|
|
97
|
+
|
|
98
|
+
cmd = "blastp -query {0} -db {1} -out {2} -outfmt 6 -evalue {3} {4} >> {5} 2>&1".format(
|
|
99
|
+
inputfile, db_prefix, output, evalue, otherPara, log_file
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
print(f"{_get_timestamp()} [INFO] Running BLASTp search: {prefix}")
|
|
103
|
+
obj = Popen(cmd, shell=True)
|
|
104
|
+
obj.wait()
|
|
105
|
+
return output
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def load_scoreD(score_file):
|
|
109
|
+
"""
|
|
110
|
+
Parses the pre-computed cluster conditional log-probability matrix.
|
|
111
|
+
Returns: Dict mapping a reference element to its [temperate_score, lytic_score].
|
|
112
|
+
"""
|
|
113
|
+
d = {}
|
|
114
|
+
with open(score_file) as f:
|
|
115
|
+
for line in f:
|
|
116
|
+
ref, temperate, virulent, members = line.strip("\n").split("\t")
|
|
117
|
+
for member in members.split(";"):
|
|
118
|
+
d[member] = [temperate, virulent]
|
|
119
|
+
return d
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_hmmsearch_opt(hmmsearch_opt, criteria=1e-5):
|
|
123
|
+
"""
|
|
124
|
+
Parses sequential single-sample HMMER --tblout files.
|
|
125
|
+
"""
|
|
126
|
+
annoD = defaultdict(dict)
|
|
127
|
+
if not os.path.exists(hmmsearch_opt):
|
|
128
|
+
return annoD
|
|
129
|
+
with open(hmmsearch_opt) as f:
|
|
130
|
+
for line in f:
|
|
131
|
+
if not line.startswith("#"):
|
|
132
|
+
t = re.split(r"\s+", line.strip("\n"))
|
|
133
|
+
if len(t) < 5:
|
|
134
|
+
continue
|
|
135
|
+
target_name, _, query_name, _, Evalue, *rest = t
|
|
136
|
+
bst_Evalue = rest[2] if len(rest) > 2 else Evalue
|
|
137
|
+
accession = t[3].split(".")[0]
|
|
138
|
+
|
|
139
|
+
if float(Evalue) <= float(criteria) and float(bst_Evalue) <= float(criteria):
|
|
140
|
+
annoD[target_name][accession] = query_name
|
|
141
|
+
return annoD
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def load_hmmsearch_opt_batched(hmmsearch_opt, criteria=1e-5):
|
|
145
|
+
"""
|
|
146
|
+
High-speed parser for batch/consolidated HMMER tabular outputs.
|
|
147
|
+
Extracts column 1 (target protein sequence identifiers) into an optimized set.
|
|
148
|
+
"""
|
|
149
|
+
hit_proteins = set()
|
|
150
|
+
if not os.path.exists(hmmsearch_opt):
|
|
151
|
+
return hit_proteins
|
|
152
|
+
|
|
153
|
+
with open(hmmsearch_opt) as f:
|
|
154
|
+
for line in f:
|
|
155
|
+
if line.startswith("#"):
|
|
156
|
+
continue
|
|
157
|
+
t = re.split(r"\s+", line.strip("\n"))
|
|
158
|
+
if len(t) < 5:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# Column mapping: t[0] yields target sequence ID, t[2] holds query matrix name
|
|
162
|
+
target_name, _, query_name, _, Evalue = t[:5]
|
|
163
|
+
|
|
164
|
+
if float(Evalue) <= float(criteria):
|
|
165
|
+
hit_proteins.add(target_name)
|
|
166
|
+
|
|
167
|
+
return hit_proteins
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def load_m8_fmt_opt(m8_input, criteria=1e-5):
|
|
171
|
+
"""
|
|
172
|
+
Parses blastp / mmseqs2 blast-style m8 (-outfmt 6) layout structures.
|
|
173
|
+
Filters by E-value and maps query sequences to their top hit reference IDs.
|
|
174
|
+
"""
|
|
175
|
+
d = {}
|
|
176
|
+
if os.path.exists(m8_input):
|
|
177
|
+
with open(m8_input) as f:
|
|
178
|
+
for line in f:
|
|
179
|
+
parts = line.strip("\n").split("\t")
|
|
180
|
+
if len(parts) < 11:
|
|
181
|
+
continue
|
|
182
|
+
query, ref, _, _, _, _, _, _, _, _, evalue = parts[:11]
|
|
183
|
+
if float(evalue) <= criteria:
|
|
184
|
+
if query not in d or float(evalue) < float(d[query][-1]):
|
|
185
|
+
d[query] = [ref, evalue]
|
|
186
|
+
return d
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def calculate_score(mmseqOpt, scoreD, criteria=1e-5):
|
|
190
|
+
"""
|
|
191
|
+
Computes Naive Bayes joint conditional probabilities for lifestyles.
|
|
192
|
+
Combines log-likelihood values from homological alignments.
|
|
193
|
+
"""
|
|
194
|
+
p_prior_temperate, p_prior_lytic = scoreD.get("Prior_probability", [0.0, 0.0])
|
|
195
|
+
|
|
196
|
+
p_temperate = [p_prior_temperate]
|
|
197
|
+
p_lytic = [p_prior_lytic]
|
|
198
|
+
|
|
199
|
+
d = load_m8_fmt_opt(mmseqOpt, criteria)
|
|
200
|
+
match_gene_number = len(d)
|
|
201
|
+
|
|
202
|
+
for query, values in d.items():
|
|
203
|
+
ref, evalue = values
|
|
204
|
+
if ref in scoreD:
|
|
205
|
+
p_temperate_gc, p_lytic_gc = scoreD[ref]
|
|
206
|
+
if float(p_temperate_gc) != 0 and float(p_lytic_gc) != 0:
|
|
207
|
+
p_temperate.append(p_temperate_gc)
|
|
208
|
+
p_lytic.append(p_lytic_gc)
|
|
209
|
+
|
|
210
|
+
label = "NA"
|
|
211
|
+
p_total_temperate, p_total_lytic = 0.0, 0.0
|
|
212
|
+
if len(p_temperate) == 1:
|
|
213
|
+
label = "Unclassified"
|
|
214
|
+
else:
|
|
215
|
+
for t, l in zip(p_temperate, p_lytic):
|
|
216
|
+
p_total_temperate += float(t)
|
|
217
|
+
p_total_lytic += float(l)
|
|
218
|
+
|
|
219
|
+
if p_total_temperate > p_total_lytic:
|
|
220
|
+
label = "Temperate"
|
|
221
|
+
elif p_total_temperate < p_total_lytic:
|
|
222
|
+
label = "Virulent"
|
|
223
|
+
else:
|
|
224
|
+
label = "Unclassified"
|
|
225
|
+
|
|
226
|
+
return p_total_temperate, p_total_lytic, label, match_gene_number
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def inoviruses_PI_like_gene_search(inputfile, prefix, wd, hmmdb, blastdb_prefix, hmm_evalue="1e-3",
|
|
230
|
+
blastp_evalue="1e-3", blastp_para="-num_threads 3", hmmer_para="--noali --cpu 1"):
|
|
231
|
+
"""Detects chronic Inovirus signature sequences using single-sample profiles."""
|
|
232
|
+
inovBlastpOpt = runBlastpsearch(inputfile, prefix, wd, blastdb_prefix, evalue=blastp_evalue, otherPara=blastp_para)
|
|
233
|
+
innoBlastD = load_m8_fmt_opt(inovBlastpOpt, float(blastp_evalue))
|
|
234
|
+
|
|
235
|
+
innoHmmerOpt = runHmmsearch(inputfile, prefix, wd, hmmdb, otherPara=hmmer_para)
|
|
236
|
+
innoHmmerD = load_hmmsearch_opt(innoHmmerOpt, float(hmm_evalue))
|
|
237
|
+
|
|
238
|
+
return bool(innoBlastD or innoHmmerD)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def check_db_md5(db_dir):
|
|
242
|
+
"""Validates the local database files against official MD5 checksum targets."""
|
|
243
|
+
cmd = "cd %s/db && md5sum --check md5sum.list" % (db_dir)
|
|
244
|
+
obj = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
|
245
|
+
obj.wait()
|
|
246
|
+
out, err = obj.communicate()
|
|
247
|
+
if b"FAILED" in out or b"FAILED" in err:
|
|
248
|
+
print(f"{_get_timestamp()} [WARNING] Database validation failure detected! Re-download recommended.")
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
else:
|
|
251
|
+
print(f"{_get_timestamp()} [INFO] Database md5 integrity checks: PASSED")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def checkdb_and_download(scriptPos, redownload=False):
|
|
255
|
+
"""Ensures reference database components are present and structurally valid."""
|
|
256
|
+
if redownload:
|
|
257
|
+
if os.path.exists(os.path.join(scriptPos, "db")):
|
|
258
|
+
cmd = f"rm -rf {os.path.join(scriptPos, 'discarded_db')} && mv -f {os.path.join(scriptPos, 'db')} {os.path.join(scriptPos, 'discarded_db')}"
|
|
259
|
+
obj = Popen(cmd, shell=True)
|
|
260
|
+
obj.wait()
|
|
261
|
+
|
|
262
|
+
if not os.path.exists(os.path.join(scriptPos, "db")):
|
|
263
|
+
print(f"{_get_timestamp()} [INFO] Reference database missing. Downloading dependencies...")
|
|
264
|
+
url = DATABASE_MANIFEST["url"]
|
|
265
|
+
file = os.path.split(url)[-1]
|
|
266
|
+
cmd = "wget {0} -P {1} && cd {1} && tar -zxvf {2} && rm -rf {2}".format(url, scriptPos, file)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
obj = Popen(cmd, shell=True)
|
|
270
|
+
obj.wait()
|
|
271
|
+
check_db_md5(scriptPos)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
print(f"{_get_timestamp()} [CRITICAL] Database setup failed: {str(e)}. Rerun with -d.")
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
else:
|
|
276
|
+
print(f"{_get_timestamp()} [INFO] Database verified.")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def bayes_classifier_single(inputfile, prefix, wd, hmm_criteria=1e-5, mmseqs_criteria=1e-5, blastp_criteria=1e-3,
|
|
280
|
+
blastp_para="-num_threads 3", hmmer_para="--noali --cpu 3",
|
|
281
|
+
mmseqs_para="-s 7 --max-seqs 1 --alignment-mode 3 --alignment-output-mode 0 --min-aln-len 40 --cov-mode 0 --greedy-best-hits 1 --threads 3"):
|
|
282
|
+
"""
|
|
283
|
+
Processes single-sample lifestyle predictions.
|
|
284
|
+
Retained to support alternative processing workflows.
|
|
285
|
+
"""
|
|
286
|
+
mkdirs(wd)
|
|
287
|
+
fileDir = os.path.dirname(os.path.abspath(__file__))
|
|
288
|
+
|
|
289
|
+
# --- Step 1: Pre-Screen for Chronic Markers to Short-Circuit Early ---
|
|
290
|
+
inno_hmmDB = os.path.join(fileDir, "db/Final_marker_morph.hmm")
|
|
291
|
+
inno_blastPre = os.path.join(fileDir, "db/Marker_ALV1")
|
|
292
|
+
inno_dect_wd = os.path.join(wd, "BC_Inno")
|
|
293
|
+
|
|
294
|
+
inno_res = inoviruses_PI_like_gene_search(
|
|
295
|
+
inputfile, f"{prefix}.Inno", inno_dect_wd, inno_hmmDB, inno_blastPre,
|
|
296
|
+
hmm_evalue=blastp_criteria, blastp_evalue=blastp_criteria, blastp_para=blastp_para, hmmer_para=hmmer_para
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if inno_res:
|
|
300
|
+
# Short circuit: return NA's for scores and "Skipped" for all structural labels, assigning "Chronic" as final label
|
|
301
|
+
return [prefix, "NA", "NA", "Skipped", "NA", "NA", "Skipped", "Chronic", "NA"]
|
|
302
|
+
|
|
303
|
+
# --- Step 2: Proceed With Structural Alignments if Not Chronic ---
|
|
304
|
+
pfam_label, bc_label, final_label = "Unclassified", "Unclassified", "Unclassified"
|
|
305
|
+
integrase_hmm = os.path.join(fileDir, "db/integrase_pfv34.hmm")
|
|
306
|
+
excisionase_hmm = os.path.join(fileDir, "db/excisionase_pfv34.hmm")
|
|
307
|
+
|
|
308
|
+
pfam_wd = os.path.join(wd, "BC_pfam")
|
|
309
|
+
inte_opt = runHmmsearch(inputfile, f"{prefix}.BC_integrase", pfam_wd, integrase_hmm, hmmer_para)
|
|
310
|
+
excision_opt = runHmmsearch(inputfile, f"{prefix}.BC_excisionase", pfam_wd, excisionase_hmm, hmmer_para)
|
|
311
|
+
|
|
312
|
+
inte_annoD = load_hmmsearch_opt(inte_opt, criteria=hmm_criteria)
|
|
313
|
+
excision_annoD = load_hmmsearch_opt(excision_opt, criteria=hmm_criteria)
|
|
314
|
+
|
|
315
|
+
inte_label = len(inte_annoD) if inte_annoD else 0
|
|
316
|
+
excision_label = len(excision_annoD) if excision_annoD else 0
|
|
317
|
+
|
|
318
|
+
if inte_label or excision_label:
|
|
319
|
+
pfam_label = "Temperate"
|
|
320
|
+
else:
|
|
321
|
+
pfam_label = "Virulent"
|
|
322
|
+
|
|
323
|
+
bc_mmseqsDB = os.path.join(fileDir, "db/bayes_mmseqs_index/training_prot_04_2025")
|
|
324
|
+
mmseqs_wd = os.path.join(wd, "BC_mmseqs")
|
|
325
|
+
mmseq_opt = runMmseqsEasysearch(inputfile, f"{prefix}.BC_mmseqs", mmseqs_wd, bc_mmseqsDB, otherPara=mmseqs_para)
|
|
326
|
+
|
|
327
|
+
score_file = os.path.join(fileDir, "db/prokaryote_only_training_cluster_04_2025.stat.scoreOpt.tsv")
|
|
328
|
+
member2scoreD = load_scoreD(score_file)
|
|
329
|
+
|
|
330
|
+
p_total_temperate, p_total_lytic, bc_label, match_gene_number = calculate_score(mmseq_opt, member2scoreD,
|
|
331
|
+
criteria=mmseqs_criteria)
|
|
332
|
+
|
|
333
|
+
if pfam_label == "Temperate" or bc_label == "Temperate":
|
|
334
|
+
final_label = "Temperate"
|
|
335
|
+
elif pfam_label == "Virulent" and bc_label == "Unclassified":
|
|
336
|
+
final_label = "Virulent"
|
|
337
|
+
else:
|
|
338
|
+
final_label = "Virulent"
|
|
339
|
+
|
|
340
|
+
return [prefix, inte_label, excision_label, pfam_label, p_total_temperate, p_total_lytic, bc_label, final_label,
|
|
341
|
+
match_gene_number]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
scriptPos = os.path.dirname(os.path.abspath(__file__))
|
|
346
|
+
checkdb_and_download(scriptPos)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# authors: sherry peng, torben sanders, erfan khamespanah
|
|
4
|
+
# date: 2026.07.09
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from argparse import RawTextHelpFormatter, ArgumentParser
|
|
9
|
+
|
|
10
|
+
# Inject parent context directory into sys path to avoid local context resolution issues
|
|
11
|
+
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
|
12
|
+
|
|
13
|
+
from Replidec.Replidec_multi import bayes_classifier_batch, bayes_classifier_contig, bayes_classifier_genomes
|
|
14
|
+
from Replidec.Replidec import checkdb_and_download
|
|
15
|
+
from Replidec import __version__
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
"""Main execution entry point parsing user flags and routing calls."""
|
|
19
|
+
parser = ArgumentParser(
|
|
20
|
+
description="Replidec: Replication cycle prediction tool for prokaryotic viruses",
|
|
21
|
+
formatter_class=RawTextHelpFormatter
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("-v", "--version", action='version', version=f'Replidec v{__version__}')
|
|
24
|
+
|
|
25
|
+
# Fixes an earlier issue by enabling required=False so that database maintenance runs freely without an input file
|
|
26
|
+
parser.add_argument("-p", "--program", default='multi_fasta', required=False,
|
|
27
|
+
choices=['multi_fasta', 'genome_table', 'protein_table'],
|
|
28
|
+
metavar="",
|
|
29
|
+
help=(
|
|
30
|
+
"{ multi_fasta | genome_table | protein_table }\n\n"
|
|
31
|
+
"multi_fasta mode:\n"
|
|
32
|
+
" input is a fasta file and treats each sequence as one virus\n\n"
|
|
33
|
+
"genome_table mode:\n"
|
|
34
|
+
" input is a tab-separated file [1st col: sample name, 2nd col: genome fasta path]\n\n"
|
|
35
|
+
"protein_table mode:\n"
|
|
36
|
+
" input is a tab-separated file [1st col: sample name, 2nd col: protein faa path]\n"
|
|
37
|
+
))
|
|
38
|
+
|
|
39
|
+
parser.add_argument("-i", "--input_file", metavar="", default=None,
|
|
40
|
+
help="The input file, which can be a sequence file or an index table\n")
|
|
41
|
+
|
|
42
|
+
parser.add_argument("-w", "--work_dir", default="replidec_results", metavar="", dest="working_directory",
|
|
43
|
+
help="Directory to store intermediate and final results (default = ./replidec_results)")
|
|
44
|
+
|
|
45
|
+
parser.add_argument("-n", "--file_name", default="prediction_summary.tsv", metavar="", dest="file_name",
|
|
46
|
+
help="Name of final summary file (default = prediction_summary.tsv)")
|
|
47
|
+
|
|
48
|
+
parser.add_argument("-t", "--threads", default=4, type=int, metavar="",
|
|
49
|
+
help="Number of compute threads assigned to execution (default = 4)")
|
|
50
|
+
|
|
51
|
+
parser.add_argument("-e", "--hmmer_Eval", default=1e-5, type=float, metavar="", dest="hmmer_Evalue_threshold",
|
|
52
|
+
help="E-value threshold to filter hmmer results (default = 1e-5)")
|
|
53
|
+
|
|
54
|
+
parser.add_argument("-E", "--hmmer_parameters", default="--noali", metavar="", dest="hmmer_parameters",
|
|
55
|
+
help="Parameters used for hmmer execution (default = --noali)")
|
|
56
|
+
|
|
57
|
+
parser.add_argument("-m", "--mmseq_Eval", default=1e-5, type=float, metavar="", dest="mmseqs_Evalue_threshold",
|
|
58
|
+
help="E-value threshold to filter mmseqs2 results (default = 1e-5)")
|
|
59
|
+
|
|
60
|
+
parser.add_argument("-M", "--mmseq_parameters", dest="mmseqs_parameters", metavar="",
|
|
61
|
+
default="-s 7 --max-seqs 1 --alignment-mode 3 --alignment-output-mode 0 --min-aln-len 40 --cov-mode 0 --greedy-best-hits 1",
|
|
62
|
+
help="Parameters used for mmseqs2 alignment processes")
|
|
63
|
+
|
|
64
|
+
parser.add_argument("-b", "--blastp_Eval", default=1e-5, type=float, metavar="", dest="blastp_Evalue_threshold",
|
|
65
|
+
help="E-value threshold to filter blastp results (default = 1e-5)")
|
|
66
|
+
|
|
67
|
+
parser.add_argument("-B", "--blastp_parameter", default="", metavar="", dest="blastp_parameters",
|
|
68
|
+
help="Parameters used for blastp alignment processes")
|
|
69
|
+
|
|
70
|
+
parser.add_argument("-d", "--db_redownload", action='store_true', default=False, dest="db_redownload",
|
|
71
|
+
help="(Re-)download reference database")
|
|
72
|
+
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
fileDir = os.path.dirname(os.path.abspath(__file__))
|
|
75
|
+
|
|
76
|
+
# Process immediate database installation if requested
|
|
77
|
+
if args.db_redownload:
|
|
78
|
+
print("[INFO] Initiating direct database deployment and verification routines...")
|
|
79
|
+
checkdb_and_download(fileDir, redownload=True)
|
|
80
|
+
if not args.input_file:
|
|
81
|
+
print("[SUCCESS] Database routine finalized successfully. Exiting cleanly.")
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
|
|
84
|
+
# Validate that an input file is provided for analysis runs
|
|
85
|
+
if not args.input_file:
|
|
86
|
+
parser.error(
|
|
87
|
+
"the following arguments are required: -i/--input_file (unless only maintaining database environments via -d)")
|
|
88
|
+
|
|
89
|
+
print(f"[INFO] Initializing processing pipelines utilizing execution framework: {args.program}")
|
|
90
|
+
|
|
91
|
+
# Route execution based on chosen program mode
|
|
92
|
+
if args.program == "genome_table":
|
|
93
|
+
bayes_classifier_genomes(args.input_file, args.working_directory,
|
|
94
|
+
summaryfile=args.file_name, threads=args.threads,
|
|
95
|
+
hmm_criteria=args.hmmer_Evalue_threshold, mmseqs_criteria=args.mmseqs_Evalue_threshold,
|
|
96
|
+
blastp_criteria=args.blastp_Evalue_threshold,
|
|
97
|
+
hmmer_para=args.hmmer_parameters, mmseqs_para=args.mmseqs_parameters,
|
|
98
|
+
blastp_para=args.blastp_parameters)
|
|
99
|
+
|
|
100
|
+
elif args.program == "multi_fasta":
|
|
101
|
+
bayes_classifier_contig(args.input_file, args.working_directory,
|
|
102
|
+
summaryfile=args.file_name, threads=args.threads,
|
|
103
|
+
hmm_criteria=args.hmmer_Evalue_threshold, mmseqs_criteria=args.mmseqs_Evalue_threshold,
|
|
104
|
+
blastp_criteria=args.blastp_Evalue_threshold,
|
|
105
|
+
hmmer_para=args.hmmer_parameters, mmseqs_para=args.mmseqs_parameters,
|
|
106
|
+
blastp_para=args.blastp_parameters)
|
|
107
|
+
|
|
108
|
+
elif args.program == "protein_table":
|
|
109
|
+
bayes_classifier_batch(args.input_file, args.working_directory,
|
|
110
|
+
summaryfile=args.file_name, threads=args.threads,
|
|
111
|
+
hmm_criteria=args.hmmer_Evalue_threshold, mmseqs_criteria=args.mmseqs_Evalue_threshold,
|
|
112
|
+
blastp_criteria=args.blastp_Evalue_threshold,
|
|
113
|
+
hmmer_para=args.hmmer_parameters, mmseqs_para=args.mmseqs_parameters,
|
|
114
|
+
blastp_para=args.blastp_parameters)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# authors: sherry peng, torben sanders, erfan khamespanah
|
|
4
|
+
# date: 2026.07.09
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
|
+
from Bio import SeqIO
|
|
10
|
+
|
|
11
|
+
# Inherit the core alignment execution stations, timestamping engines, and global database manifests
|
|
12
|
+
from Replidec.Replidec import (
|
|
13
|
+
DATABASE_MANIFEST,
|
|
14
|
+
_get_timestamp,
|
|
15
|
+
checkdb_and_download,
|
|
16
|
+
runProdigal,
|
|
17
|
+
runHmmsearch,
|
|
18
|
+
runMmseqsEasysearch,
|
|
19
|
+
runBlastpsearch,
|
|
20
|
+
load_scoreD,
|
|
21
|
+
load_hmmsearch_opt_batched,
|
|
22
|
+
load_m8_fmt_opt,
|
|
23
|
+
bayes_classifier_single
|
|
24
|
+
)
|
|
25
|
+
from Replidec.utility import mkdirs
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bayes_classifier_contig(inputfile, wd, summaryfile="BC_predict.summary", threads=4,
|
|
29
|
+
hmm_criteria=1e-5, mmseqs_criteria=1e-5, blastp_criteria=1e-5,
|
|
30
|
+
blastp_para="", hmmer_para="", mmseqs_para=""):
|
|
31
|
+
'''
|
|
32
|
+
Aim: High-speed single-pass batch prediction for multi-FASTA contig files.
|
|
33
|
+
'''
|
|
34
|
+
print(f"{_get_timestamp()} [INFO] Initializing reference database verification...")
|
|
35
|
+
fileDir = os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
checkdb_and_download(fileDir)
|
|
37
|
+
|
|
38
|
+
if not inputfile:
|
|
39
|
+
print(f"{_get_timestamp()} [CRITICAL] No input file provided.")
|
|
40
|
+
return
|
|
41
|
+
if not os.path.exists(inputfile):
|
|
42
|
+
raise FileNotFoundError(f"Input file '{inputfile}' does not exist.")
|
|
43
|
+
|
|
44
|
+
mkdirs(wd)
|
|
45
|
+
|
|
46
|
+
# --- Thread Injection Logic ---
|
|
47
|
+
if '--threads' in mmseqs_para:
|
|
48
|
+
mmseqs_para = re.sub(r'--threads\s+\d+', f'--threads {threads}', mmseqs_para)
|
|
49
|
+
else:
|
|
50
|
+
mmseqs_para += f' --threads {threads}'
|
|
51
|
+
|
|
52
|
+
if '--cpu' in hmmer_para:
|
|
53
|
+
hmmer_para = re.sub(r'--cpu\s+\d+', f'--cpu {threads}', hmmer_para)
|
|
54
|
+
else:
|
|
55
|
+
hmmer_para += f' --cpu {threads}'
|
|
56
|
+
|
|
57
|
+
if '-num_threads' in blastp_para:
|
|
58
|
+
blastp_para = re.sub(r'-num_threads\s+\d+', f'-num_threads {threads}', blastp_para)
|
|
59
|
+
else:
|
|
60
|
+
blastp_para += f' -num_threads {threads}'
|
|
61
|
+
|
|
62
|
+
# --- Phase 1: Metagenomic Gene Prediction ---
|
|
63
|
+
print(f"{_get_timestamp()} [INFO] Sanitizing multi-FASTA boundaries and running Prodigal...")
|
|
64
|
+
sanitized_fasta = os.path.join(wd, "all_contigs_sanitized.fna")
|
|
65
|
+
contig_lengths = {}
|
|
66
|
+
|
|
67
|
+
with open(sanitized_fasta, "w") as out_f:
|
|
68
|
+
for record in SeqIO.parse(inputfile, "fasta"):
|
|
69
|
+
clean_id = record.id.replace("|", "_")
|
|
70
|
+
record.id = clean_id
|
|
71
|
+
record.description = ""
|
|
72
|
+
contig_lengths[clean_id] = len(record.seq)
|
|
73
|
+
SeqIO.write(record, out_f, "fasta")
|
|
74
|
+
|
|
75
|
+
prodigal_wd = os.path.join(wd, "BC_prodigal")
|
|
76
|
+
master_faa = runProdigal(sanitized_fasta, "all_contigs", prodigal_wd, program="meta", otherPara="-g 11")
|
|
77
|
+
|
|
78
|
+
if not os.path.exists(master_faa) or os.path.getsize(master_faa) == 0:
|
|
79
|
+
print(f"{_get_timestamp()} [WARNING] No proteins predicted. Exiting workflow cleanly.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
protein_to_contig = {}
|
|
83
|
+
contig_to_proteins = {cid: [] for cid in contig_lengths.keys()}
|
|
84
|
+
for record in SeqIO.parse(master_faa, "fasta"):
|
|
85
|
+
last_underscore = record.id.rfind("_")
|
|
86
|
+
if last_underscore != -1:
|
|
87
|
+
contig_id = record.id[:last_underscore]
|
|
88
|
+
if contig_id in contig_to_proteins:
|
|
89
|
+
protein_to_contig[record.id] = contig_id
|
|
90
|
+
contig_to_proteins[contig_id].append(record.id)
|
|
91
|
+
|
|
92
|
+
# --- Phase 2: Vectorized Chronic Inovirus Marker Screening (Moved Up) ---
|
|
93
|
+
print(f"{_get_timestamp()} [INFO] Phase 2: Screening for Chronic Inovirus elements...")
|
|
94
|
+
inno_wd = os.path.join(wd, "BC_Inno")
|
|
95
|
+
inno_hmmDB = os.path.join(fileDir, DATABASE_MANIFEST["inovirus_hmm"])
|
|
96
|
+
inno_blastPre = os.path.join(fileDir, DATABASE_MANIFEST["inovirus_blast"])
|
|
97
|
+
|
|
98
|
+
inno_blast_opt = runBlastpsearch(master_faa, "all_contigs.Inno", inno_wd, inno_blastPre,
|
|
99
|
+
evalue=str(blastp_criteria), otherPara=blastp_para)
|
|
100
|
+
inno_blast_hits = load_m8_fmt_opt(inno_blast_opt, float(blastp_criteria))
|
|
101
|
+
|
|
102
|
+
inno_hmm_opt = runHmmsearch(master_faa, "all_contigs.Inno", inno_wd, inno_hmmDB, hmmer_para)
|
|
103
|
+
inno_hmm_hits = load_hmmsearch_opt_batched(inno_hmm_opt, float(blastp_criteria))
|
|
104
|
+
|
|
105
|
+
# Identify contigs that hit as Chronic
|
|
106
|
+
chronic_contigs = set()
|
|
107
|
+
for contig_id, proteins in contig_to_proteins.items():
|
|
108
|
+
if any(p in inno_blast_hits or p in inno_hmm_hits for p in proteins):
|
|
109
|
+
chronic_contigs.add(contig_id)
|
|
110
|
+
|
|
111
|
+
# Short-circuit logic: create a filtered protein FASTA containing only non-chronic targets
|
|
112
|
+
filtered_faa = os.path.join(prodigal_wd, "non_chronic_contigs.faa")
|
|
113
|
+
non_chronic_count = 0
|
|
114
|
+
with open(filtered_faa, "w") as out_f:
|
|
115
|
+
for record in SeqIO.parse(master_faa, "fasta"):
|
|
116
|
+
contig_id = protein_to_contig.get(record.id)
|
|
117
|
+
if contig_id not in chronic_contigs:
|
|
118
|
+
SeqIO.write(record, out_f, "fasta")
|
|
119
|
+
non_chronic_count += 1
|
|
120
|
+
|
|
121
|
+
# --- Phase 3: Vectorized Alignment against Structural Profiles ---
|
|
122
|
+
inte_anno, excision_anno, mmseq_hits = {}, {}, {}
|
|
123
|
+
|
|
124
|
+
if non_chronic_count > 0:
|
|
125
|
+
print(f"{_get_timestamp()} [INFO] Phase 3: Launching HMMER and MMseqs2 structural profiles for remaining contigs...")
|
|
126
|
+
pfam_wd = os.path.join(wd, "BC_pfam")
|
|
127
|
+
integrase_hmm = os.path.join(fileDir, DATABASE_MANIFEST["integrase_hmm"])
|
|
128
|
+
excisionase_hmm = os.path.join(fileDir, DATABASE_MANIFEST["excisionase_hmm"])
|
|
129
|
+
|
|
130
|
+
# Run tools strictly on the filtered, non-chronic FASTA
|
|
131
|
+
inte_opt = runHmmsearch(filtered_faa, "all_contigs.BC_integrase", pfam_wd, integrase_hmm, hmmer_para)
|
|
132
|
+
excision_opt = runHmmsearch(filtered_faa, "all_contigs.BC_excisionase", pfam_wd, excisionase_hmm, hmmer_para)
|
|
133
|
+
|
|
134
|
+
inte_anno = load_hmmsearch_opt_batched(inte_opt, criteria=hmm_criteria)
|
|
135
|
+
excision_anno = load_hmmsearch_opt_batched(excision_opt, criteria=hmm_criteria)
|
|
136
|
+
|
|
137
|
+
bc_mmseqsDB = os.path.join(fileDir, DATABASE_MANIFEST["mmseqs_index"])
|
|
138
|
+
mmseqs_wd = os.path.join(wd, "BC_mmseqs")
|
|
139
|
+
mmseq_opt = runMmseqsEasysearch(filtered_faa, "all_contigs.BC_mmseqs", mmseqs_wd, bc_mmseqsDB, otherPara=mmseqs_para)
|
|
140
|
+
mmseq_hits = load_m8_fmt_opt(mmseq_opt, criteria=mmseqs_criteria)
|
|
141
|
+
|
|
142
|
+
score_file = os.path.join(fileDir, DATABASE_MANIFEST["scoring_matrix"])
|
|
143
|
+
score_matrix = load_scoreD(score_file)
|
|
144
|
+
p_prior_temperate, p_prior_lytic = score_matrix.get("Prior_probability", [0.0, 0.0])
|
|
145
|
+
|
|
146
|
+
# --- Phase 4: Scoring Matrix Assembly & Final Report Output ---
|
|
147
|
+
print(f"{_get_timestamp()} [INFO] Assembling scoring matrices and writing global execution summary...")
|
|
148
|
+
summary_path = os.path.join(wd, summaryfile)
|
|
149
|
+
|
|
150
|
+
total_contigs = len(contig_lengths)
|
|
151
|
+
processed_count = 0
|
|
152
|
+
|
|
153
|
+
with open(summary_path, "w") as opt:
|
|
154
|
+
header = "sample_name\tintegrase_number\texcisionase_number\tpfam_label\tbc_temperate\tbc_virulent\tbc_label\tfinal_label\tmatch_gene_number\n"
|
|
155
|
+
opt.write(header)
|
|
156
|
+
|
|
157
|
+
for contig_id in contig_lengths.keys():
|
|
158
|
+
# Apply hard overrides to Chronic short-circuited contigs
|
|
159
|
+
if contig_id in chronic_contigs:
|
|
160
|
+
row = [contig_id, "NA", "NA", "Skipped", "NA", "NA", "Skipped", "Chronic", "NA"]
|
|
161
|
+
opt.write("\t".join(str(x) for x in row) + "\n")
|
|
162
|
+
processed_count += 1
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Process remaining non-chronic contigs normally
|
|
166
|
+
proteins = contig_to_proteins[contig_id]
|
|
167
|
+
|
|
168
|
+
inte_count = sum(1 for p in proteins if p in inte_anno)
|
|
169
|
+
excision_count = sum(1 for p in proteins if p in excision_anno)
|
|
170
|
+
pfam_label = "Temperate" if (inte_count > 0 or excision_count > 0) else "Virulent"
|
|
171
|
+
|
|
172
|
+
p_temperate = [p_prior_temperate]
|
|
173
|
+
p_lytic = [p_prior_lytic]
|
|
174
|
+
match_gene_number = 0
|
|
175
|
+
|
|
176
|
+
for p in proteins:
|
|
177
|
+
if p in mmseq_hits:
|
|
178
|
+
match_gene_number += 1
|
|
179
|
+
ref, _ = mmseq_hits[p]
|
|
180
|
+
if ref in score_matrix:
|
|
181
|
+
pt, pl = score_matrix[ref]
|
|
182
|
+
if float(pt) != 0 and float(pl) != 0:
|
|
183
|
+
p_temperate.append(pt)
|
|
184
|
+
p_lytic.append(pl)
|
|
185
|
+
|
|
186
|
+
if len(p_temperate) == 1:
|
|
187
|
+
p_total_temperate, p_total_lytic = 0.0, 0.0
|
|
188
|
+
bc_label = "Unclassified"
|
|
189
|
+
else:
|
|
190
|
+
p_total_temperate = sum(float(t) for t in p_temperate)
|
|
191
|
+
p_total_lytic = sum(float(l) for l in p_lytic)
|
|
192
|
+
if p_total_temperate > p_total_lytic:
|
|
193
|
+
bc_label = "Temperate"
|
|
194
|
+
elif p_total_temperate < p_total_lytic:
|
|
195
|
+
bc_label = "Virulent"
|
|
196
|
+
else:
|
|
197
|
+
bc_label = "Unclassified"
|
|
198
|
+
|
|
199
|
+
if pfam_label == "Temperate" or bc_label == "Temperate":
|
|
200
|
+
final_label = "Temperate"
|
|
201
|
+
|
|
202
|
+
elif pfam_label == "Virulent" and bc_label == "Unclassified":
|
|
203
|
+
final_label = "Virulent"
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
final_label = "Virulent"
|
|
207
|
+
|
|
208
|
+
row = [contig_id, inte_count, excision_count, pfam_label, p_total_temperate, p_total_lytic, bc_label,
|
|
209
|
+
final_label, match_gene_number]
|
|
210
|
+
opt.write("\t".join(str(x) for x in row) + "\n")
|
|
211
|
+
|
|
212
|
+
processed_count += 1
|
|
213
|
+
if processed_count % 5000 == 0 or processed_count == total_contigs:
|
|
214
|
+
print(f"{_get_timestamp()} [PROGRESS] Evaluated {processed_count:,} / {total_contigs:,} contigs.")
|
|
215
|
+
|
|
216
|
+
print(f"{_get_timestamp()} [SUCCESS] Job finalized cleanly. Summary written to: {summary_path}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def bayes_classifier_batch(inputfile, wd, summaryfile="BC_predict.summary", threads=10,
|
|
220
|
+
hmm_criteria=1e-5, mmseqs_criteria=1e-5, blastp_criteria=1e-5,
|
|
221
|
+
blastp_para="", hmmer_para="", mmseqs_para=""):
|
|
222
|
+
'''
|
|
223
|
+
Aim: Multithreaded engine execution for index data tables mapping pre-computed protein FAA listings.
|
|
224
|
+
'''
|
|
225
|
+
fileDir = os.path.dirname(os.path.abspath(__file__))
|
|
226
|
+
checkdb_and_download(fileDir)
|
|
227
|
+
mkdirs(wd)
|
|
228
|
+
|
|
229
|
+
with open(os.path.join(wd, summaryfile), "w") as opt:
|
|
230
|
+
header = "sample_name\tintegrase_number\texcisionase_number\tpfam_label\tbc_temperate\tbc_virulent\tbc_label\tfinal_label\tmatch_gene_number\tpath\n"
|
|
231
|
+
opt.write(header)
|
|
232
|
+
|
|
233
|
+
executor = ThreadPoolExecutor(max_workers=threads)
|
|
234
|
+
all_task = []
|
|
235
|
+
kwargsD = {"hmm_criteria": hmm_criteria, "mmseqs_criteria": mmseqs_criteria, "blastp_criteria": blastp_criteria,
|
|
236
|
+
"blastp_para": blastp_para, "hmmer_para": hmmer_para, "mmseqs_para": mmseqs_para}
|
|
237
|
+
|
|
238
|
+
with open(inputfile) as f:
|
|
239
|
+
for line in f:
|
|
240
|
+
sample_name, path = line.strip("\n").split("\t")
|
|
241
|
+
all_task.append(executor.submit(bayes_classifier_single, path, sample_name, wd, **kwargsD))
|
|
242
|
+
|
|
243
|
+
for future in as_completed(all_task):
|
|
244
|
+
res = future.result()
|
|
245
|
+
res.append(path)
|
|
246
|
+
opt.write("\t".join([str(i) for i in res]) + "\n")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def bayes_classifier_genomes(inputfile, wd, summaryfile="BC_predict.summary", threads=10,
|
|
250
|
+
hmm_criteria=1e-5, mmseqs_criteria=1e-5, blastp_criteria=1e-5,
|
|
251
|
+
blastp_para="", hmmer_para="", mmseqs_para=""):
|
|
252
|
+
'''
|
|
253
|
+
Aim: Multithreaded engine execution for structured tables mapping whole genome assembly configurations.
|
|
254
|
+
'''
|
|
255
|
+
fileDir = os.path.dirname(os.path.abspath(__file__))
|
|
256
|
+
checkdb_and_download(fileDir)
|
|
257
|
+
mkdirs(wd)
|
|
258
|
+
|
|
259
|
+
with open(os.path.join(wd, summaryfile), "w") as opt:
|
|
260
|
+
header = "sample_name\tintegrase_number\texcisionase_number\tpfam_label\tbc_temperate\tbc_virulent\tbc_label\tfinal_label\tmatch_gene_number\tpath\n"
|
|
261
|
+
opt.write(header)
|
|
262
|
+
|
|
263
|
+
executor = ThreadPoolExecutor(max_workers=threads)
|
|
264
|
+
all_task = []
|
|
265
|
+
kwargsD = {"hmm_criteria": hmm_criteria, "mmseqs_criteria": mmseqs_criteria, "blastp_criteria": blastp_criteria,
|
|
266
|
+
"blastp_para": blastp_para, "hmmer_para": hmmer_para, "mmseqs_para": mmseqs_para}
|
|
267
|
+
faaDict = {}
|
|
268
|
+
|
|
269
|
+
with open(inputfile) as f:
|
|
270
|
+
for line in f:
|
|
271
|
+
sample_name, path = line.strip("\n").split("\t")
|
|
272
|
+
faaFile = runProdigal(path, sample_name, f"{wd}/BC_prodigal", program="meta", otherPara="-g 11")
|
|
273
|
+
faaDict[sample_name] = faaFile
|
|
274
|
+
|
|
275
|
+
for sample_name, faaFile in faaDict.items():
|
|
276
|
+
if os.path.getsize(faaFile) != 0:
|
|
277
|
+
all_task.append(executor.submit(bayes_classifier_single, faaFile, sample_name, wd, **kwargsD))
|
|
278
|
+
|
|
279
|
+
for future in as_completed(all_task):
|
|
280
|
+
res = future.result()
|
|
281
|
+
faaFile = faaDict[res[0]]
|
|
282
|
+
res.append(faaFile)
|
|
283
|
+
opt.write("\t".join([str(i) for i in res]) + "\n")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
pass
|
Replidec/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Replidec package - bacteriophage lifestyle prediction (temperate|virulent|chronic) based on the naive bayes classifier.
|
|
4
|
+
|
|
5
|
+
Written by Xue Peng (Email: xue.peng@helmholtz-muenchen.de)
|
|
6
|
+
|
|
7
|
+
Python modules
|
|
8
|
+
----------------
|
|
9
|
+
The package consists of the following Python modules:
|
|
10
|
+
* bayes_classifier_batch
|
|
11
|
+
* bayes_classifier_contig
|
|
12
|
+
* bayes_classifier_genomes
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__author__ = """Xue Peng, Torben Sanders, Erfan Khamespanah"""
|
|
17
|
+
__email__ = 'xue.peng@helmholtz-muenchen.de'
|
|
18
|
+
__version__ = '0.3.6'
|
Replidec/utility.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding: utf-8
|
|
3
|
+
# authors: sherry peng, torben sanders, erfan khamespanah
|
|
4
|
+
# mail: xue.peng@helmholtz-muenchen.de
|
|
5
|
+
# date: 2026.07.09
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from subprocess import Popen, PIPE
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mkdirs(dirname):
|
|
13
|
+
'''Safely creates output directories if they do not already exist.'''
|
|
14
|
+
if not os.path.exists(dirname):
|
|
15
|
+
os.makedirs(dirname)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def checkEnv(sft):
|
|
19
|
+
'''
|
|
20
|
+
Validates that required third-party binaries are accessible in the system PATH.
|
|
21
|
+
|
|
22
|
+
Uses standard system commands to check for binary availability. If a dependency
|
|
23
|
+
cannot be resolved, it raises a clean critical alert and exits to prevent
|
|
24
|
+
pipeline failures down the line.
|
|
25
|
+
'''
|
|
26
|
+
cmd = "which %s" % sft
|
|
27
|
+
|
|
28
|
+
# Direct stdout and stderr streams to execution PIPEs to preserve clean console logging
|
|
29
|
+
obj = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
|
30
|
+
obj.wait()
|
|
31
|
+
|
|
32
|
+
# An execution return code other than 0 indicates missing dependencies
|
|
33
|
+
if obj.returncode != 0:
|
|
34
|
+
print(f"\n[CRITICAL] Dependency Missing: '{sft}' could not be resolved in your current environment.")
|
|
35
|
+
print(f"Please ensure '{sft}' is installed and available in your PATH variable.\n")
|
|
36
|
+
sys.exit(1)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Xue Peng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Replidec
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: Replication Cycle Decipher for Phages
|
|
5
|
+
Home-page: https://github.com/pengSherryYel/Replidec
|
|
6
|
+
Author: Xue Peng
|
|
7
|
+
Author-email: peng_sherry@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: Replidec
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: biopython>=1.77
|
|
18
|
+
Requires-Dist: future>=0.18.2
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
# RepliDec: Replication Cycle Decipher for Phages
|
|
22
|
+
|
|
23
|
+
[](https://pypi.python.org/pypi/Replidec)
|
|
24
|
+
[](https://anaconda.org/bioconda/replidec)
|
|
25
|
+
[](https://anaconda.org/bioconda/replidec)
|
|
26
|
+
|
|
27
|
+
## Aim
|
|
28
|
+
|
|
29
|
+
RepliDec employs a Bayesian classifier combined with homology searching to accurately predict the replication cycle of prokaryotic viruses (Phages).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
### Method 1: using Conda (Recommend using bioconda with the latest version)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
conda create -n replidec
|
|
37
|
+
conda activate replidec
|
|
38
|
+
conda install -c conda-forge -c bioconda replidec
|
|
39
|
+
or
|
|
40
|
+
conda install -c denglab -c conda-forge -c bioconda replidec
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Method 2: using Docker
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
docker pull quay.io/biocontainers/replidec:0.3.5--pyhdfd78af_0
|
|
47
|
+
docker run quay.io/biocontainers/replidec:0.3.5--pyhdfd78af_0 Replidec -h
|
|
48
|
+
## Example
|
|
49
|
+
docker run -v /your/host/data:/data/ quay.io/biocontainers/replidec:0.3.5--pyhdfd78af_0 Replidec -i data/your_inputfile -p
|
|
50
|
+
choose_mode_based_on_your_input_type -w data
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Method 3: using pip
|
|
54
|
+
|
|
55
|
+
If you install using pip, please make sure that `mmseqs`, `hmmsearch`, and `blastp` are set to $PATH, these software can be equal to or higher than the version list below
|
|
56
|
+
|
|
57
|
+
- MMseqs2 Version: 13.45111
|
|
58
|
+
|
|
59
|
+
- HMMER 3.3.2 (Nov 2020)
|
|
60
|
+
|
|
61
|
+
- Protein-Protein BLAST 2.5.0+
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip3 install Replidec
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage: Overview
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Replidec [-p PROGRAM] [-i INPUT] [-w WORK_DIR] [-h HELP] [options]
|
|
71
|
+
|
|
72
|
+
options:
|
|
73
|
+
-h, --help show this help message and exit
|
|
74
|
+
-v, --version show program's version number and exit
|
|
75
|
+
-p , --program { multi_fasta | genome_table | protein_table }
|
|
76
|
+
|
|
77
|
+
multi_fasta mode:
|
|
78
|
+
input is a fasta file and treat each sequence as one virus
|
|
79
|
+
|
|
80
|
+
genome_table mode:
|
|
81
|
+
input is a tab separated file with two columns
|
|
82
|
+
___1st column: sample name
|
|
83
|
+
___2nd column: path to the genome sequence file of the virus
|
|
84
|
+
|
|
85
|
+
protein_table mode:
|
|
86
|
+
input is a tab separated file with two columns
|
|
87
|
+
___1st column: sample name
|
|
88
|
+
___2nd column: path to the protein file of the virus
|
|
89
|
+
|
|
90
|
+
-i , --input_file The input file, which can be a sequence file or an index table
|
|
91
|
+
-w , --work_dir Directory to store intermediate and final results (default = ./Replidec_results)
|
|
92
|
+
-n , --file_name Name of final summary file (default = prediction_summary.tsv)
|
|
93
|
+
-t , --threads Number of parallel threads (default = 4)
|
|
94
|
+
-e , --hmmer_Eval E-value threshold to filter hmmer result (default = 1e-5)
|
|
95
|
+
-E , --hmmer_parameters
|
|
96
|
+
Parameters used for hmmer (default = --noali --cpu 3)
|
|
97
|
+
-m , --mmseq_Eval E-value threshold to filter mmseqs2 result (default = 1e-5)
|
|
98
|
+
-M , --mmseq_parameters
|
|
99
|
+
Parameter used for mmseqs
|
|
100
|
+
(default = -s 7 --max-seqs 1 --alignment-mode 3 --alignment-output-mode 0 --min-aln-len 40 --cov-mode 0 --greedy-best-hits 1 --threads 3)
|
|
101
|
+
-b , --blastp_Eval E-value threshold to filter blast result (default =1e-5)
|
|
102
|
+
-B , --blastp_parameter
|
|
103
|
+
Parameters used for blastp (default = -num_threads 3)
|
|
104
|
+
-d, --db_redownload Remove and re-download database
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Usage: Download database (-d)
|
|
108
|
+
The database used in Replidec will be downloaded automatically.
|
|
109
|
+
|
|
110
|
+
Location: will be downloaded at the location where Replidec is installed
|
|
111
|
+
|
|
112
|
+
If you want to redownload the database, the `-d` parameter can be used. The older database will be moved to "discarded_db" in the workdir(-w); This dir can be removed manually by the user.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Usage: Input (-i) and program (-p)
|
|
116
|
+
|
|
117
|
+
##**IMPORTANT**##
|
|
118
|
+
<br>
|
|
119
|
+
RepliDec assumes that all input contigs are either complete or partial phage genomes. Please ensure you pass your contigs through a viral discovery tool to exclude non-phage sequences to ensure accurate results.
|
|
120
|
+
|
|
121
|
+
**The input file is different based on different programs**
|
|
122
|
+
|
|
123
|
+
Replidec offers **3** different programs:
|
|
124
|
+
|
|
125
|
+
1. 'multi_fasta'
|
|
126
|
+
2. 'genome_table'
|
|
127
|
+
3. 'protein_table',
|
|
128
|
+
|
|
129
|
+
### multi_fasta mode:
|
|
130
|
+
* input is a **fasta** file and treat each sequence as one virus.
|
|
131
|
+
* Example: <your_path>/viral_contigs.fasta
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
>contig_1
|
|
135
|
+
TATCGATCGATCGATCGATCGATCGTACGTACGTACGTACG...
|
|
136
|
+
>contig_2
|
|
137
|
+
CATCGATCGATCGATCGATCGATCGATCGATCGATCGATCG...
|
|
138
|
+
...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### genome_table mode:
|
|
142
|
+
* input is a **tab** separated file with two columns.
|
|
143
|
+
|
|
144
|
+
* 1st column: sample name
|
|
145
|
+
* 2nd column: path to the genome sequence file of the virus
|
|
146
|
+
* Example: <your_path>/example_genomes.tsv
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
contig_1 your/file/path/contig_1.fasta
|
|
150
|
+
contig_2 your/file/path/contig_2.fasta
|
|
151
|
+
contig_3 your/file/path/contig_3.fasta
|
|
152
|
+
...
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### protein_table mode:
|
|
156
|
+
* input is a **tab** separated file with two columns
|
|
157
|
+
|
|
158
|
+
* 1st column: sample name
|
|
159
|
+
* 2nd column: path to the protein file of the virus
|
|
160
|
+
* Example: <your_path>/example_proteins.tsv
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
contig_1_prot your/file/path/contig_1.fasta
|
|
164
|
+
contig_2_prot your/file/path/contig_2.fasta
|
|
165
|
+
contig_3_prot your/file/path/contig_3.fasta
|
|
166
|
+
...
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Usage: Output (-w and -n)
|
|
170
|
+
The output directory can be assigned with `-w , --work_dir `, where the intermediate files and the final prediction results will be stored. The name of the final summary file can be assigned with the `-n , --file_name` argument.
|
|
171
|
+
|
|
172
|
+
At the end of the analysis, the output directory would contain the following:
|
|
173
|
+
* BC_Inno: This directory contains the result file for dectect Innovirues
|
|
174
|
+
* BC_mmseqs: This directory contains the result file for mapping result to our custom database
|
|
175
|
+
* BC_pfam: This directory contains the result file for dectect the Integrase and Excisionase
|
|
176
|
+
* BC_prodigal: This directory contains the result file for CDS prediction from genome or contig sequence. (If {-p protein_table} is used, this directory will not be created.)
|
|
177
|
+
* prediction_summary.tsv: This file is the summary file of the prediction result. It contains multiple columns.
|
|
178
|
+
* sample_name: identifier. Can be a sequence ID or the first column of the plain text input file.
|
|
179
|
+
|
|
180
|
+
* integrase_number: the number of genes mapped to integrase meet the creteria(set by -c).
|
|
181
|
+
|
|
182
|
+
* excisionase_number: the number of genes mapped to excisionase meet the creteria(set by -c).
|
|
183
|
+
|
|
184
|
+
* pfam_label: if it contains integrase or excisionase, the label will be "Temperate". Otherwise "Virulent".
|
|
185
|
+
|
|
186
|
+
* bc_temperate: conditional probability of temperate|genes.
|
|
187
|
+
|
|
188
|
+
* bc_virulent: conditional probability of virulent|genes.
|
|
189
|
+
|
|
190
|
+
* bc_label: if bc_temperate greater than bc_virulent, label will be "Temperate". Otherwise "Virulent".
|
|
191
|
+
|
|
192
|
+
* final_label: if pfam_label and bc_label both is Temperate, then label will be "Temperate"; if an Innovirues marker gene exists, then label will be "Chronic"; otherwise "Virulent".
|
|
193
|
+
|
|
194
|
+
* match_gene_number: the number of genes mapped to our custom database.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## Example (Data in test folder, please navigate to test folder first)
|
|
198
|
+
```
|
|
199
|
+
cd test
|
|
200
|
+
|
|
201
|
+
## Conda
|
|
202
|
+
## test passed - genome_table
|
|
203
|
+
replidec -p genome_table -i example/genome_test.small.index -w opt_folder_genome_table
|
|
204
|
+
|
|
205
|
+
## test passed - multi_fasta
|
|
206
|
+
replidec -p multi_fasta -i example/test.contig.small.fa -w opt_folder_multi_fasta
|
|
207
|
+
|
|
208
|
+
## test passed - protein_table
|
|
209
|
+
replidec -p protein_table -i example/example.small.list -w opt_folder_protein_table
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
## Docker
|
|
213
|
+
docker run -v /Your_path_clone_replidec/Replidec/test:/data/ quay.io/biocontainers/replidec:0.3.5--pyhdfd78af_0 Replidec -p multi_fasta -i /data/example/test.contig.small.new.fa -w /data/opt_folder_docker_multi_fasta
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Issues
|
|
218
|
+
### Database can not be downloaded automatically
|
|
219
|
+
If the dataset cannot be automatically downloaded from Zenodo due to regional access restrictions, you may manually add it instead. The same database has also been uploaded to OSF as an alternative source.
|
|
220
|
+
|
|
221
|
+
1. **Locate your Replidec installation path**
|
|
222
|
+
After installing Replidec via Conda or Docker, locate the installed directory. Typically, it can be found at:
|
|
223
|
+
`your_conda_path/envs/env_name/lib/python*/site-packages/Replidec`
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
2. **Navigate to the Replidec folder**
|
|
227
|
+
Use the terminal to move into the directory:
|
|
228
|
+
`cd your_conda_path/envs/env_name/lib/python*/site-packages/Replidec`
|
|
229
|
+
|
|
230
|
+
3. **Download the database manually from OSF (Project name: Replidec)**
|
|
231
|
+
Access the alternative download link here:
|
|
232
|
+
👉 https://osf.io/thpkb/files/osfstorage
|
|
233
|
+
|
|
234
|
+
4. **Extract the database**
|
|
235
|
+
After downloading, extract the contents of the archive into the Replidec directory, and a folder named "db" will be created:
|
|
236
|
+
`tar -zxvf db_v0.3.2.tar.gz`
|
|
237
|
+
✅ Note: Make sure the extracted folder can be found in this path `your_conda_path/envs/env_name/lib/python*/site-packages/Replidec/db`.
|
|
238
|
+
|
|
239
|
+
For now, everything is fixed. Enjoy playing with Replidec!
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Replidec/Replidec.py,sha256=Prc8wqVSPQ6ohnMpGfZhU_41KAJAU9oPwNrLIc0whu8,13576
|
|
2
|
+
Replidec/Replidec_cmdline.py,sha256=j6pTc6zX5k5WC3gSSdGlecqrzx10TAaqNDuBp_IKgMw,6699
|
|
3
|
+
Replidec/Replidec_multi.py,sha256=wFKkAOzWLtp_PNxdMi1x1tjhYMJ5cpfImtJMo_35iEI,12910
|
|
4
|
+
Replidec/__init__.py,sha256=RcETINSpABsnvIJyPVWVtM2AX8Pv0da1eMkQop5FFqg,487
|
|
5
|
+
Replidec/utility.py,sha256=NAsMUkTMOH3fxxhkPuojqFDax5Myz5bblMuqeZU0Ldw,1213
|
|
6
|
+
replidec-0.3.6.dist-info/LICENSE,sha256=0ICJkzBSEER28iOUCdn68F8LcBfoDToZ1eXxSHCv0bI,1067
|
|
7
|
+
replidec-0.3.6.dist-info/METADATA,sha256=39W524t0z51dA8Avv-mkscwsdqMviB4oIsI_ByJGYjs,9780
|
|
8
|
+
replidec-0.3.6.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
|
|
9
|
+
replidec-0.3.6.dist-info/entry_points.txt,sha256=rz41hPhcFj9RWiEmUTgbYur-jiGViF6Ub4LXQTc2D-E,102
|
|
10
|
+
replidec-0.3.6.dist-info/top_level.txt,sha256=P8TNQZNPdiptZP_WEt4bffLZRuwZdOgsOUD3XEsGQYg,9
|
|
11
|
+
replidec-0.3.6.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
12
|
+
replidec-0.3.6.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Replidec
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|