drep 4.0.2__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.
- drep/VERSION +1 -0
- drep/WorkDirectory.py +355 -0
- drep/__init__.py +101 -0
- drep/argumentParser.py +279 -0
- drep/controller.py +105 -0
- drep/d_adjust.py +272 -0
- drep/d_analyze.py +1613 -0
- drep/d_bonus.py +429 -0
- drep/d_choose.py +362 -0
- drep/d_cluster/__init__.py +0 -0
- drep/d_cluster/cluster_utils.py +126 -0
- drep/d_cluster/compare_utils.py +636 -0
- drep/d_cluster/controller.py +228 -0
- drep/d_cluster/external.py +765 -0
- drep/d_cluster/greedy_clustering.py +181 -0
- drep/d_cluster/parsers.py +0 -0
- drep/d_cluster/union_find.py +543 -0
- drep/d_cluster/utils.py +687 -0
- drep/d_evaluate.py +355 -0
- drep/d_filter.py +831 -0
- drep/d_workflows.py +135 -0
- drep-4.0.2.data/scripts/ScaffoldLevel_dRep.py +1101 -0
- drep-4.0.2.data/scripts/dRep +32 -0
- drep-4.0.2.data/scripts/parse_stb.py +140 -0
- drep-4.0.2.dist-info/METADATA +23 -0
- drep-4.0.2.dist-info/RECORD +28 -0
- drep-4.0.2.dist-info/WHEEL +5 -0
- drep-4.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
import drep
|
|
11
|
+
import drep.d_cluster.utils
|
|
12
|
+
# from drep.d_cluster.utils import _get_genome_name_from_fasta, process_deltafiles, _randomString, gen_gANI_cmd, \
|
|
13
|
+
# process_gani_files, gen_goANI_cmd, process_goani_files
|
|
14
|
+
|
|
15
|
+
def run_pairwise_ANImf(genome_list, ANIn_folder, **kwargs):
|
|
16
|
+
'''
|
|
17
|
+
Given a list of genomes and an output folder, compare all genomes using ANImf
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
genome_list: list of locations of genome files
|
|
21
|
+
ANIn_folder: folder to store the output of comparison
|
|
22
|
+
|
|
23
|
+
Keyword arguments:
|
|
24
|
+
processors: threads to use
|
|
25
|
+
debug: if true save extra output
|
|
26
|
+
wd: needed if debug is True
|
|
27
|
+
'''
|
|
28
|
+
p = kwargs.get('processors',6)
|
|
29
|
+
genomes = genome_list
|
|
30
|
+
|
|
31
|
+
# Make folder if doesnt exist
|
|
32
|
+
if not os.path.exists(ANIn_folder):
|
|
33
|
+
os.makedirs(ANIn_folder)
|
|
34
|
+
|
|
35
|
+
# Gen commands
|
|
36
|
+
cmds = []
|
|
37
|
+
files = []
|
|
38
|
+
for g1 in genomes:
|
|
39
|
+
# Make it so each reference is it's own folder, to spread out .delta files
|
|
40
|
+
cur_folder = os.path.join(ANIn_folder, drep.d_cluster.utils._get_genome_name_from_fasta(g1))
|
|
41
|
+
if not os.path.exists(cur_folder):
|
|
42
|
+
os.makedirs(cur_folder)
|
|
43
|
+
|
|
44
|
+
for g2 in genomes:
|
|
45
|
+
file_name = "{0}/{1}_vs_{2}".format(cur_folder,
|
|
46
|
+
drep.d_cluster.utils._get_genome_name_from_fasta(g1),
|
|
47
|
+
drep.d_cluster.utils._get_genome_name_from_fasta(g2))
|
|
48
|
+
files.append(file_name)
|
|
49
|
+
|
|
50
|
+
# If the file doesn't already exist, add it to what needs to be run
|
|
51
|
+
if not os.path.isfile(file_name + '.delta.filtered'):
|
|
52
|
+
cmds.append(gen_animf_cmd(file_name,g1,g2))
|
|
53
|
+
|
|
54
|
+
# Run commands
|
|
55
|
+
if len(cmds) > 0:
|
|
56
|
+
for c in cmds:
|
|
57
|
+
logging.debug(c)
|
|
58
|
+
|
|
59
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False)):
|
|
60
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
61
|
+
else:
|
|
62
|
+
logdir = False
|
|
63
|
+
drep.thread_cmds(cmds, shell=True, logdir=logdir, t=int(p))
|
|
64
|
+
|
|
65
|
+
# Make dictionary of genome lengths
|
|
66
|
+
org_lengths = {}
|
|
67
|
+
for genome in genomes:
|
|
68
|
+
org_lengths[drep.d_cluster.utils._get_genome_name_from_fasta(genome)] = drep.d_filter.calc_fasta_length(genome)
|
|
69
|
+
|
|
70
|
+
# Parse output
|
|
71
|
+
deltafiles = ["{0}.delta.filtered".format(f) for f in files]
|
|
72
|
+
df = drep.d_cluster.utils.process_deltafiles(deltafiles, org_lengths, **kwargs)
|
|
73
|
+
|
|
74
|
+
return df
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def run_pairwise_fastANI(genome_list, outdir, **kwargs):
|
|
78
|
+
p = kwargs.get('processors',6)
|
|
79
|
+
code = drep.d_cluster.utils._randomString(stringLength=10)
|
|
80
|
+
|
|
81
|
+
# Make folders
|
|
82
|
+
if not os.path.exists(outdir):
|
|
83
|
+
os.makedirs(outdir)
|
|
84
|
+
tmp_dir = os.path.join(outdir, 'tmp/')
|
|
85
|
+
if not os.path.exists(tmp_dir):
|
|
86
|
+
os.makedirs(tmp_dir)
|
|
87
|
+
|
|
88
|
+
# Make genome list
|
|
89
|
+
glist = os.path.join(tmp_dir, 'genomeList')
|
|
90
|
+
glist = _make_glist(genome_list, glist)
|
|
91
|
+
|
|
92
|
+
# Gen command
|
|
93
|
+
exe_loc = drep.get_exe('fastANI')
|
|
94
|
+
out_base = os.path.join(outdir, 'fastANI_out_{0}'.format(code))
|
|
95
|
+
cmd = [exe_loc, '--ql', glist, '--rl', glist, '-o', out_base, '--matrix', '-t', str(p), "--minFraction", str(0)]
|
|
96
|
+
logging.debug(' '.join(cmd) + ' ' + code)
|
|
97
|
+
|
|
98
|
+
# Run command
|
|
99
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False)):
|
|
100
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
101
|
+
else:
|
|
102
|
+
logdir = False
|
|
103
|
+
drep.thread_cmds([cmd], shell=False, logdir=logdir, t=1)
|
|
104
|
+
|
|
105
|
+
# Load results
|
|
106
|
+
fdb = load_fastani(out_base)
|
|
107
|
+
|
|
108
|
+
# fix missing ones
|
|
109
|
+
try:
|
|
110
|
+
fdb = _fix_fastani(fdb)
|
|
111
|
+
return fdb
|
|
112
|
+
|
|
113
|
+
# handle broken self
|
|
114
|
+
except:
|
|
115
|
+
logging.error("CRITICAL ERROR WITH SECONDARY CLUSTERING CODE {0}; SKIPPING".format(code))
|
|
116
|
+
return pd.DataFrame()
|
|
117
|
+
|
|
118
|
+
def run_pairwise_skani(genome_list, outdir, **kwargs):
|
|
119
|
+
p = kwargs.get('processors',6)
|
|
120
|
+
code = drep.d_cluster.utils._randomString(stringLength=10)
|
|
121
|
+
extra_cmd = kwargs.get('skani_extra', "")
|
|
122
|
+
|
|
123
|
+
# Make folders
|
|
124
|
+
if not os.path.exists(outdir):
|
|
125
|
+
os.makedirs(outdir)
|
|
126
|
+
tmp_dir = os.path.join(outdir, 'tmp/')
|
|
127
|
+
if not os.path.exists(tmp_dir):
|
|
128
|
+
os.makedirs(tmp_dir)
|
|
129
|
+
|
|
130
|
+
# Make genome list
|
|
131
|
+
glist = os.path.join(tmp_dir, 'genomeList')
|
|
132
|
+
glist = _make_glist(genome_list, glist)
|
|
133
|
+
|
|
134
|
+
# Gen command
|
|
135
|
+
exe_loc = drep.get_exe('skani')
|
|
136
|
+
out_base = os.path.join(outdir, 'skani_out_{0}'.format(code))
|
|
137
|
+
#cmd = [exe_loc, '--ql', glist, '--rl', glist, '-o', out_base, '--matrix', '-t', str(p), "--minFraction", str(0)]
|
|
138
|
+
cmd = [exe_loc, "triangle", "-t", str(p), '-o', out_base, '--full-matrix', '-l', glist, '--detailed', '-s', "1", '--min-af', "0"]
|
|
139
|
+
if extra_cmd != "":
|
|
140
|
+
cmd += extra_cmd.split(' ')
|
|
141
|
+
|
|
142
|
+
logging.debug(' '.join(cmd) + ' ' + code)
|
|
143
|
+
|
|
144
|
+
# Run command
|
|
145
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False)):
|
|
146
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
147
|
+
else:
|
|
148
|
+
logdir = False
|
|
149
|
+
drep.thread_cmds([cmd], shell=False, logdir=logdir, t=1)
|
|
150
|
+
|
|
151
|
+
# Load results
|
|
152
|
+
fdb = load_skani(out_base)
|
|
153
|
+
|
|
154
|
+
return fdb
|
|
155
|
+
|
|
156
|
+
def load_triangle_matrix(file_path):
|
|
157
|
+
with open(file_path, 'r') as f:
|
|
158
|
+
lines = f.readlines()
|
|
159
|
+
|
|
160
|
+
# Get the number of genomes
|
|
161
|
+
num_genomes = int(lines[0].strip())
|
|
162
|
+
|
|
163
|
+
# Initialize lists to store data
|
|
164
|
+
reference = []
|
|
165
|
+
query = []
|
|
166
|
+
ani = []
|
|
167
|
+
|
|
168
|
+
# Parse the matrix
|
|
169
|
+
for i in range(1, num_genomes + 1):
|
|
170
|
+
row = lines[i].strip().split('\t')
|
|
171
|
+
ref_genome = row[0]
|
|
172
|
+
for j in range(1, i + 1):
|
|
173
|
+
query_genome = lines[j].strip().split('\t')[0]
|
|
174
|
+
ani_value = float(row[j])
|
|
175
|
+
reference.append(ref_genome)
|
|
176
|
+
query.append(query_genome)
|
|
177
|
+
ani.append(ani_value)
|
|
178
|
+
|
|
179
|
+
# Create a DataFrame
|
|
180
|
+
df = pd.DataFrame({'reference': reference, 'querry': query, 'ani': ani})
|
|
181
|
+
return df
|
|
182
|
+
|
|
183
|
+
def load_matrix_to_dataframe(file_path):
|
|
184
|
+
with open(file_path, 'r') as file:
|
|
185
|
+
lines = file.readlines()
|
|
186
|
+
|
|
187
|
+
num_genomes = int(lines[0].strip())
|
|
188
|
+
genomes = [line.split('\t')[0] for line in lines[1:num_genomes + 1]]
|
|
189
|
+
|
|
190
|
+
data = []
|
|
191
|
+
for i in range(num_genomes):
|
|
192
|
+
for j in range(num_genomes):
|
|
193
|
+
genome1 = genomes[i]
|
|
194
|
+
genome2 = genomes[j]
|
|
195
|
+
ani = float(lines[i + 1].split('\t')[j + 1])
|
|
196
|
+
data.append([genome1, genome2, ani])
|
|
197
|
+
|
|
198
|
+
df = pd.DataFrame(data, columns=['reference', 'querry', 'ani'])
|
|
199
|
+
return df
|
|
200
|
+
|
|
201
|
+
def fastani_one_vs_many(one, many, genome_rep_file, outdir, **kwargs):
|
|
202
|
+
p = kwargs.get('processors', 6)
|
|
203
|
+
code = drep.d_cluster.utils._randomString(stringLength=10)
|
|
204
|
+
tmp_dir = kwargs.get('tmp_dir')
|
|
205
|
+
logdir = kwargs.get('logdir')
|
|
206
|
+
exe_loc = kwargs.get('current_exe')
|
|
207
|
+
redo = kwargs.get('redo', False)
|
|
208
|
+
|
|
209
|
+
# Gen command
|
|
210
|
+
out_base = os.path.join(outdir, 'fastANI_out_{0}'.format(code))
|
|
211
|
+
cmd = [exe_loc, '-q', one, '--rl', genome_rep_file, '-o', out_base, '--matrix', '-t', str(p), "--minFraction", str(0)]
|
|
212
|
+
logging.debug(' '.join(cmd) + ' ' + code)
|
|
213
|
+
|
|
214
|
+
# Run command
|
|
215
|
+
drep.thread_cmds([cmd], shell=False, logdir=logdir, t=1)
|
|
216
|
+
|
|
217
|
+
# Load results
|
|
218
|
+
fdb = load_fastani(out_base)
|
|
219
|
+
|
|
220
|
+
return fdb
|
|
221
|
+
|
|
222
|
+
def load_fastani(file):
|
|
223
|
+
fdb = pd.read_csv(file, names=['reference', 'querry', 'ANI', 'j1', 'j2'], sep=r'\s+')
|
|
224
|
+
for c in ['reference', 'querry']:
|
|
225
|
+
fdb[c] = [drep.d_cluster.utils._get_genome_name_from_fasta(x) for x in fdb[c]]
|
|
226
|
+
fdb = fdb.rename(columns={'ANI':'ani'})
|
|
227
|
+
fdb['alignment_coverage'] = [(j1/j2) for j1, j2 in zip(fdb['j1'], fdb['j2'])]
|
|
228
|
+
fdb = fdb[['reference', 'querry', 'ani', 'alignment_coverage']]
|
|
229
|
+
fdb['ani'] = [x/100 for x in fdb['ani']]
|
|
230
|
+
|
|
231
|
+
return fdb
|
|
232
|
+
|
|
233
|
+
def load_skani(file):
|
|
234
|
+
# Load the ani triangle
|
|
235
|
+
adb = load_triangle_matrix(file)
|
|
236
|
+
adb['ani'] = adb['ani'] / 100
|
|
237
|
+
|
|
238
|
+
# Make symnetrical
|
|
239
|
+
db = adb.rename(columns={'reference':'querry', 'querry':'reference'})
|
|
240
|
+
db = db[db['reference'] != db['querry']]
|
|
241
|
+
adb = pd.concat([adb, db], ignore_index=True).reset_index(drop=True)
|
|
242
|
+
|
|
243
|
+
# Load the af triangle. skani reports aligned fractions as percentages
|
|
244
|
+
# (0-100); every other dRep algorithm reports alignment_coverage on a 0-1
|
|
245
|
+
# scale, and that is what cov_thresh is compared against in
|
|
246
|
+
# make_linkage_Ndb. Without this conversion the coverage filter is inert for
|
|
247
|
+
# skani (e.g. a pair aligning over only 1% of the genome has
|
|
248
|
+
# alignment_coverage=1.04, which sails past a cov_thresh of 0.5), which can
|
|
249
|
+
# merge distantly related genomes that share a small conserved region.
|
|
250
|
+
tdb = load_matrix_to_dataframe(file + '.af').rename(columns={'ani':'alignment_coverage'})
|
|
251
|
+
tdb['alignment_coverage'] = tdb['alignment_coverage'] / 100
|
|
252
|
+
|
|
253
|
+
# Merge
|
|
254
|
+
assert len(adb) == len(tdb)
|
|
255
|
+
fdb = pd.merge(adb, tdb, how='inner')
|
|
256
|
+
|
|
257
|
+
# parse
|
|
258
|
+
for c in ['reference', 'querry']:
|
|
259
|
+
fdb[c] = [drep.d_cluster.utils._get_genome_name_from_fasta(x) for x in fdb[c]]
|
|
260
|
+
|
|
261
|
+
return fdb
|
|
262
|
+
|
|
263
|
+
def _fix_fastani(odb):
|
|
264
|
+
# Add back missing genomes
|
|
265
|
+
fdb = odb.pivot(index="reference", columns="querry", values="ani")
|
|
266
|
+
fdb.reset_index(level=0, inplace=True)
|
|
267
|
+
fdb.fillna(0, inplace=True)
|
|
268
|
+
fdb = fdb.melt(id_vars=['reference']).rename(
|
|
269
|
+
columns={'value':'ani'})
|
|
270
|
+
|
|
271
|
+
# Add back alignment coverage
|
|
272
|
+
fdb = pd.merge(fdb, odb[['reference', 'querry', 'alignment_coverage']], on=['reference', 'querry'], how='outer')
|
|
273
|
+
fdb['alignment_coverage'] = fdb['alignment_coverage'].fillna(0)
|
|
274
|
+
|
|
275
|
+
assert len(fdb['reference'].unique()) == len(fdb['querry'].unique())
|
|
276
|
+
assert len(fdb) == (len(fdb['reference'].unique()) * len(fdb['querry'].unique()))
|
|
277
|
+
|
|
278
|
+
return fdb
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def run_skani_triangle_sparse(genome_list, outdir, screen, min_af=15, **kwargs):
|
|
282
|
+
"""
|
|
283
|
+
Run `skani triangle --sparse` and return the path to the sparse output file.
|
|
284
|
+
|
|
285
|
+
The sparse output is an edge list of only the above-screening-threshold pairs,
|
|
286
|
+
so it never materializes the N^2 matrix on disk or in memory. It is meant to
|
|
287
|
+
be streamed (see union_find.cluster_skani_sparse_files), not loaded whole.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
genome_list: list of genome file locations.
|
|
291
|
+
outdir: directory to write the sparse output and temp files.
|
|
292
|
+
screen: skani -s screening threshold (percent identity). Pairs below this
|
|
293
|
+
are discarded during sketching and never appear in the output. Should
|
|
294
|
+
be <= the primary ANI threshold so no real edges are missed.
|
|
295
|
+
min_af: skani --min-af, the minimum percent of a genome that must align
|
|
296
|
+
for the pair to be reported. See the note below -- do not set this to
|
|
297
|
+
0 for primary clustering.
|
|
298
|
+
|
|
299
|
+
Keyword Args:
|
|
300
|
+
processors: threads for skani (default 6).
|
|
301
|
+
skani_extra: extra args passed through to skani triangle.
|
|
302
|
+
wd, debug: for command logging.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Path to the sparse skani output file.
|
|
306
|
+
"""
|
|
307
|
+
p = kwargs.get('processors', 6)
|
|
308
|
+
code = drep.d_cluster.utils._randomString(stringLength=10)
|
|
309
|
+
extra_cmd = kwargs.get('skani_extra', "")
|
|
310
|
+
|
|
311
|
+
if not os.path.exists(outdir):
|
|
312
|
+
os.makedirs(outdir)
|
|
313
|
+
tmp_dir = os.path.join(outdir, 'tmp/')
|
|
314
|
+
if not os.path.exists(tmp_dir):
|
|
315
|
+
os.makedirs(tmp_dir)
|
|
316
|
+
|
|
317
|
+
glist = os.path.join(tmp_dir, 'genomeList_{0}'.format(code))
|
|
318
|
+
glist = _make_glist(genome_list, glist)
|
|
319
|
+
|
|
320
|
+
exe_loc = drep.get_exe('skani')
|
|
321
|
+
out_file = os.path.join(outdir, 'skani_sparse_{0}.tsv'.format(code))
|
|
322
|
+
# min_af matters far more than it looks, because MASH similarity and skani ANI
|
|
323
|
+
# measure different things. MASH compares k-mers across the whole genome, so
|
|
324
|
+
# two genomes sharing only a small conserved region score as distant. skani's
|
|
325
|
+
# ANI is the identity *within aligned regions only*, so that same pair reports
|
|
326
|
+
# a high ANI and (with min_af 0) becomes a primary-clustering edge. Under
|
|
327
|
+
# single linkage those few spurious bridges chain everything together: on 10k
|
|
328
|
+
# UHGG genomes, --min-af 0 collapsed 59% of the dataset into one primary
|
|
329
|
+
# cluster (largest 5857) while skani's 15% default reproduced the MASH
|
|
330
|
+
# partition almost exactly (984 clusters vs MASH's 989, largest 626 vs 626).
|
|
331
|
+
# The aligned-fraction filter is what makes skani's ANI comparable to MASH's
|
|
332
|
+
# whole-genome similarity -- it is not an obstacle to work around.
|
|
333
|
+
cmd = [exe_loc, "triangle", "--sparse", "-t", str(p), '-o', out_file,
|
|
334
|
+
'-l', glist, '-s', str(screen), '--min-af', str(min_af)]
|
|
335
|
+
if extra_cmd != "":
|
|
336
|
+
cmd += extra_cmd.split(' ')
|
|
337
|
+
|
|
338
|
+
logging.debug(' '.join(cmd) + ' ' + code)
|
|
339
|
+
|
|
340
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False)):
|
|
341
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
342
|
+
else:
|
|
343
|
+
logdir = False
|
|
344
|
+
drep.thread_cmds([cmd], shell=False, logdir=logdir, t=1)
|
|
345
|
+
|
|
346
|
+
return out_file
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _make_glist(genomes, floc):
|
|
350
|
+
o = open(floc, 'w')
|
|
351
|
+
for g in genomes:
|
|
352
|
+
loc = os.path.abspath(g)
|
|
353
|
+
assert os.path.isfile(g)
|
|
354
|
+
o.write(loc + '\n')
|
|
355
|
+
o.close()
|
|
356
|
+
return floc
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def run_pairwise_gANI(bdb, gANI_folder, prod_folder, **kwargs):
|
|
360
|
+
'''
|
|
361
|
+
Run pairwise gANI on a list of Genomes
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
bdb: DataFrame with ['genome', 'location']
|
|
365
|
+
gANI_folder: folder to store gANI output
|
|
366
|
+
prod_folder: folder containing prodigal output from genomes (will run if needed)
|
|
367
|
+
|
|
368
|
+
Keyword arguments:
|
|
369
|
+
debug: log all of the commands
|
|
370
|
+
wd: if you want to log commands, you also need the wd
|
|
371
|
+
processors: threads to use
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
DataFrame: Ndb for gANI
|
|
375
|
+
'''
|
|
376
|
+
p = kwargs.get('processors',6)
|
|
377
|
+
gANI_exe = drep.get_exe('ANIcalculator')
|
|
378
|
+
genomes = bdb['location'].tolist()
|
|
379
|
+
|
|
380
|
+
# Make folders
|
|
381
|
+
if not os.path.exists(gANI_folder):
|
|
382
|
+
os.makedirs(gANI_folder)
|
|
383
|
+
if not os.path.exists(prod_folder):
|
|
384
|
+
os.makedirs(prod_folder)
|
|
385
|
+
|
|
386
|
+
# Remove crap folders- they shouldn't exist and if they do it messes things up
|
|
387
|
+
crap_folders = glob.glob(gANI_folder + '*.gANITEMP')
|
|
388
|
+
for crap_folder in crap_folders:
|
|
389
|
+
if os.path.exists(crap_folder):
|
|
390
|
+
shutil.rmtree(crap_folder)
|
|
391
|
+
|
|
392
|
+
# Run prodigal
|
|
393
|
+
logging.debug("Running prodigal...")
|
|
394
|
+
drep.d_filter.run_prodigal(bdb['location'].tolist(), prod_folder, **kwargs)
|
|
395
|
+
|
|
396
|
+
# Gen gANI commands
|
|
397
|
+
logging.debug("Running gANI...")
|
|
398
|
+
cmds = []
|
|
399
|
+
files = []
|
|
400
|
+
for i, g1 in enumerate(genomes):
|
|
401
|
+
# Make it so each reference is it's own folder, to spread out .delta files
|
|
402
|
+
cur_folder = os.path.join(gANI_folder, drep.d_cluster.utils._get_genome_name_from_fasta(g1))
|
|
403
|
+
if not os.path.exists(cur_folder):
|
|
404
|
+
os.makedirs(cur_folder)
|
|
405
|
+
|
|
406
|
+
for j, g2 in enumerate(genomes):
|
|
407
|
+
if i > j:
|
|
408
|
+
name1= drep.d_cluster.utils._get_genome_name_from_fasta(g1)
|
|
409
|
+
name2= drep.d_cluster.utils._get_genome_name_from_fasta(g2)
|
|
410
|
+
file_name = "{0}/{1}_vs_{2}.gANI".format(cur_folder,
|
|
411
|
+
name1, name2)
|
|
412
|
+
files.append(file_name)
|
|
413
|
+
|
|
414
|
+
# If the file doesn't already exist, add it to what needs to be run
|
|
415
|
+
if not os.path.isfile(file_name):
|
|
416
|
+
fna1 = "{0}.fna".format(os.path.join(prod_folder,name1))
|
|
417
|
+
fna2 = "{0}.fna".format(os.path.join(prod_folder,name2))
|
|
418
|
+
cmds.append(drep.d_cluster.utils.gen_gANI_cmd(file_name,fna1,fna2,gANI_exe))
|
|
419
|
+
|
|
420
|
+
# Run commands
|
|
421
|
+
if len(cmds) > 0:
|
|
422
|
+
logging.debug('Running gANI commands: {0}'.format('\n'.join([' '.join(x) for x in cmds])))
|
|
423
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False) == True):
|
|
424
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
425
|
+
else:
|
|
426
|
+
logdir = False
|
|
427
|
+
#logdir = "/home/mattolm/Programs/drep/tests/test_backend/logs/"
|
|
428
|
+
drep.thread_cmds(cmds, logdir=logdir, t=int(p))
|
|
429
|
+
|
|
430
|
+
else:
|
|
431
|
+
logging.debug("gANI already run- will not re-run")
|
|
432
|
+
|
|
433
|
+
# Pdrep.d_cluster.utils.arse output
|
|
434
|
+
df = drep.d_cluster.utils.process_gani_files(files)
|
|
435
|
+
|
|
436
|
+
# Add self-comparisons if there is only one genome
|
|
437
|
+
if len(genomes) == 1:
|
|
438
|
+
Table = {'querry':[],'reference':[],'ani':[],'alignment_coverage':[]}
|
|
439
|
+
for g in genomes:
|
|
440
|
+
Table['reference'].append(drep.d_cluster.utils._get_genome_name_from_fasta(g))
|
|
441
|
+
Table['querry'].append(drep.d_cluster.utils._get_genome_name_from_fasta(g))
|
|
442
|
+
Table['ani'].append(1)
|
|
443
|
+
Table['alignment_coverage'].append(1)
|
|
444
|
+
d = pd.DataFrame(Table)
|
|
445
|
+
df = pd.concat([df,d],ignore_index=True)
|
|
446
|
+
|
|
447
|
+
return df
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def run_pairwise_goANI(bdb, goANI_folder, prod_folder, **kwargs):
|
|
451
|
+
'''
|
|
452
|
+
Run pairwise goANI on a list of Genomes
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
bdb: DataFrame with ['genome', 'location']
|
|
456
|
+
goANI_folder: folder to store gANI output
|
|
457
|
+
prod_folder: folder containing prodigal output from genomes (will run if needed)
|
|
458
|
+
|
|
459
|
+
Keyword arguments:
|
|
460
|
+
debug: log all of the commands
|
|
461
|
+
wd: if you want to log commands, you also need the wd
|
|
462
|
+
processors: threads to use
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
DataFrame: Ndb for gANI
|
|
466
|
+
'''
|
|
467
|
+
p = kwargs.get('processors',6)
|
|
468
|
+
nsimscan_exe = drep.get_exe('nsimscan')
|
|
469
|
+
genomes = bdb['location'].tolist()
|
|
470
|
+
|
|
471
|
+
# Make folders
|
|
472
|
+
if not os.path.exists(goANI_folder):
|
|
473
|
+
os.makedirs(goANI_folder)
|
|
474
|
+
if not os.path.exists(prod_folder):
|
|
475
|
+
os.makedirs(prod_folder)
|
|
476
|
+
|
|
477
|
+
# Run prodigal
|
|
478
|
+
logging.debug("Running prodigal...")
|
|
479
|
+
drep.d_filter.run_prodigal(bdb['location'].tolist(), prod_folder, **kwargs)
|
|
480
|
+
|
|
481
|
+
# Gen gANI commands
|
|
482
|
+
logging.debug("Running goANI...")
|
|
483
|
+
cmds = []
|
|
484
|
+
files = []
|
|
485
|
+
for i, g1 in enumerate(genomes):
|
|
486
|
+
# Make it so each reference is it's own folder, to spread out .delta files
|
|
487
|
+
cur_folder = os.path.join(goANI_folder, drep.d_cluster.utils._get_genome_name_from_fasta(g1))
|
|
488
|
+
if not os.path.exists(cur_folder):
|
|
489
|
+
os.makedirs(cur_folder)
|
|
490
|
+
|
|
491
|
+
for j, g2 in enumerate(genomes):
|
|
492
|
+
if i != j:
|
|
493
|
+
name1= drep.d_cluster.utils._get_genome_name_from_fasta(g1)
|
|
494
|
+
name2= drep.d_cluster.utils._get_genome_name_from_fasta(g2)
|
|
495
|
+
file_name = "{0}/{1}_vs_{2}.sim".format(cur_folder,
|
|
496
|
+
name1, name2)
|
|
497
|
+
files.append(file_name)
|
|
498
|
+
|
|
499
|
+
# If the file doesn't already exist, add it to what needs to be run
|
|
500
|
+
if not os.path.isfile(file_name):
|
|
501
|
+
fna1 = "{0}.fna".format(os.path.join(prod_folder,name1))
|
|
502
|
+
fna2 = "{0}.fna".format(os.path.join(prod_folder,name2))
|
|
503
|
+
cmds.append(drep.d_cluster.utils.gen_goANI_cmd(file_name,fna1,fna2,nsimscan_exe))
|
|
504
|
+
|
|
505
|
+
# Run commands
|
|
506
|
+
if len(cmds) > 0:
|
|
507
|
+
logging.debug('Running goANI commands: {0}'.format('\n'.join([' '.join(x) for x in cmds])))
|
|
508
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False) == True):
|
|
509
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
510
|
+
else:
|
|
511
|
+
logdir = False
|
|
512
|
+
#logdir = "/home/mattolm/Programs/drep/tests/test_backend/logs/"
|
|
513
|
+
drep.thread_cmds(cmds, logdir=logdir, t=int(p))
|
|
514
|
+
|
|
515
|
+
else:
|
|
516
|
+
logging.debug("goANI already run- will not re-run")
|
|
517
|
+
|
|
518
|
+
# Parse output
|
|
519
|
+
df = drep.d_cluster.utils.process_goani_files(files)
|
|
520
|
+
|
|
521
|
+
# Add self-comparisons if there is only one genome
|
|
522
|
+
if len(genomes) == 1:
|
|
523
|
+
Table = {'querry':[],'reference':[],'ani':[],'alignment_coverage':[]}
|
|
524
|
+
for g in genomes:
|
|
525
|
+
Table['reference'].append(drep.d_cluster.utils._get_genome_name_from_fasta(g))
|
|
526
|
+
Table['querry'].append(drep.d_cluster.utils._get_genome_name_from_fasta(g))
|
|
527
|
+
Table['ani'].append(1)
|
|
528
|
+
Table['alignment_coverage'].append(1)
|
|
529
|
+
d = pd.DataFrame(Table)
|
|
530
|
+
df = pd.concat([df,d],ignore_index=True)
|
|
531
|
+
|
|
532
|
+
return df
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def parse_gani_file(file):
|
|
536
|
+
'''
|
|
537
|
+
Parse gANI file, return dictionary of results
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
file: location of gANI file
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
dict: results in the gANI file
|
|
544
|
+
'''
|
|
545
|
+
try:
|
|
546
|
+
x = pd.read_table(file)
|
|
547
|
+
except:
|
|
548
|
+
logging.error('gANI file {0} does not exist. The most likely reason is that '.format(file)\
|
|
549
|
+
+ 'one of the genomes has a .fasta header than gANI doesnt like.'\
|
|
550
|
+
+ ' Known issues include having a header over 160 characaters long, or any '\
|
|
551
|
+
+ 'kind of special character besides "_" (including ".", ":", and "-").'\
|
|
552
|
+
+ ' To fix this error either use ANIn (which is not as picky with fasta headers)'\
|
|
553
|
+
+ ', or fix the .fasta headers to conform to those rules.')
|
|
554
|
+
sys.exit()
|
|
555
|
+
x = x.rename(columns={'GENOME1':'reference','GENOME2':'querry','AF(1->2)':'rq_coverage',
|
|
556
|
+
'AF(2->1)':'qr_coverage','ANI(1->2)':'rq_ani','ANI(2->1)':'qr_ani'})
|
|
557
|
+
dict = x.to_dict(orient='list')
|
|
558
|
+
dict = {k:v[0] for k,v in dict.items()}
|
|
559
|
+
dict['reference'] = dict['reference'][:-4]
|
|
560
|
+
dict['querry'] = dict['querry'][:-4]
|
|
561
|
+
return dict
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def parse_nsim_file(file):
|
|
565
|
+
'''
|
|
566
|
+
Parse nsim file, return dictionary of results and gene datatable
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
file: location of nsimscan file
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
dict: results in the gANI file
|
|
573
|
+
db: gene-based alignment results
|
|
574
|
+
'''
|
|
575
|
+
# Load
|
|
576
|
+
db = pd.read_csv(file, sep='\t')
|
|
577
|
+
|
|
578
|
+
if len(db) == 0:
|
|
579
|
+
logging.warning("File {0} is empty, indicating a nsimscan failure! Run with --debug and check the log folder for details".format(file))
|
|
580
|
+
|
|
581
|
+
return _summarize_nsimsan(db)
|
|
582
|
+
|
|
583
|
+
db = db.rename(columns={'#qry_id':'qry_id', '#Q_id':'qry_id', 'S_id':'sbj_id', 'trg_len':'sbj_len'})
|
|
584
|
+
|
|
585
|
+
# Filter
|
|
586
|
+
try:
|
|
587
|
+
db = _filter_nsimscan(db)
|
|
588
|
+
except:
|
|
589
|
+
print("ERROR! FILE {0}".format(file))
|
|
590
|
+
print(db)
|
|
591
|
+
return _summarize_nsimsan(pd.DataFrame())
|
|
592
|
+
|
|
593
|
+
# Make summary results
|
|
594
|
+
x = _summarize_nsimsan(db)
|
|
595
|
+
|
|
596
|
+
return x
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _filter_nsimscan(db1, minAF=0.7, minANI=70):
|
|
600
|
+
'''
|
|
601
|
+
Filter a single nsim scan file
|
|
602
|
+
'''
|
|
603
|
+
db1 = db1.sort_values(['al_len','qry_id', 'sbj_id'], ascending=False)
|
|
604
|
+
|
|
605
|
+
# Filter like gANI
|
|
606
|
+
db1['af'] = [a/min(o,t) for a,o,t in zip(db1['al_len'],
|
|
607
|
+
db1['qry_len'],
|
|
608
|
+
db1['sbj_len'])]
|
|
609
|
+
db1 = db1[(db1['af'] >= minAF) & (db1['p_inden'] >= minANI)]
|
|
610
|
+
|
|
611
|
+
return db1
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _summarize_nsimsan(db):
|
|
615
|
+
'''
|
|
616
|
+
Take all those aligned genes and return a summary ANI
|
|
617
|
+
'''
|
|
618
|
+
table = {}
|
|
619
|
+
if len(db) > 0:
|
|
620
|
+
table['ani'] = sum([p * l for p, l in zip(db['p_inden'], db['al_len'])]) \
|
|
621
|
+
/ db['al_len'].sum()
|
|
622
|
+
table['af'] = db['al_len'].sum() / db['qry_len'].sum()
|
|
623
|
+
else:
|
|
624
|
+
table['ani'] = 0
|
|
625
|
+
table['af'] = 0
|
|
626
|
+
|
|
627
|
+
return table
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def gen_nucmer_cmd(prefix,ref,querry,c='65',noextend=False,maxgap='90',method='mum'):
|
|
631
|
+
'''
|
|
632
|
+
Generate command to run with nucmer
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
prefix: desired output name (will have .delta appended)
|
|
636
|
+
ref: location of reference genome
|
|
637
|
+
querry: location of querry genomes
|
|
638
|
+
|
|
639
|
+
Keyword args:
|
|
640
|
+
c: c value
|
|
641
|
+
noextend: either True or False
|
|
642
|
+
maxgap: maxgap
|
|
643
|
+
method: detault is 'mum'
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
list: command to run number
|
|
647
|
+
'''
|
|
648
|
+
cmd = ['nucmer','--' + method,'-p',prefix,'-c',str(c),'-g',str(maxgap)]
|
|
649
|
+
if noextend: cmd.append('--noextend')
|
|
650
|
+
cmd += [ref,querry]
|
|
651
|
+
return cmd
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def gen_animf_cmd(prefix,ref,querry, **kwargs):
|
|
655
|
+
'''
|
|
656
|
+
return animf command. It will be a single string, with the format:
|
|
657
|
+
"nucmer cmd ; filter cmd"
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
prefix: desired file output name
|
|
661
|
+
ref: location of reference genome
|
|
662
|
+
querry: location of querry genome
|
|
663
|
+
|
|
664
|
+
Keyword args:
|
|
665
|
+
all: passed on to gen_nucmer_cmd
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
string: format is "nucmer cmd ; filter cmd"
|
|
669
|
+
'''
|
|
670
|
+
|
|
671
|
+
nucmer_cmd = gen_nucmer_cmd(prefix,ref,querry, **kwargs)
|
|
672
|
+
|
|
673
|
+
delta = prefix + '.delta'
|
|
674
|
+
out = delta + '.filtered'
|
|
675
|
+
|
|
676
|
+
filter_cmd = gen_filter_cmd(prefix + '.delta', out)
|
|
677
|
+
return ' '.join(nucmer_cmd) + '; ' + ' '.join(filter_cmd)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def gen_filter_cmd(delta, out):
|
|
681
|
+
'''
|
|
682
|
+
return delta-filter command
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
delta: desired .delta file to filter
|
|
686
|
+
out: desired output file
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
list: cmd to run
|
|
690
|
+
'''
|
|
691
|
+
cmd = ["delta-filter", '-r', '-q', delta, '>', out]
|
|
692
|
+
return cmd
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _gen_nomash_cdb(Bdb):
|
|
696
|
+
'''
|
|
697
|
+
From Bdb, just add a column of 'primary_cluster' = 0
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
Bdb: dataframe with [genome, location]
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
DataFrame: Cdb
|
|
704
|
+
'''
|
|
705
|
+
Cdb = Bdb.copy()
|
|
706
|
+
Cdb['primary_cluster'] = 0
|
|
707
|
+
return Cdb
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def add_avani(db):
|
|
711
|
+
'''
|
|
712
|
+
add a column titled 'av_ani' to the passed in dataframe
|
|
713
|
+
|
|
714
|
+
dataframe must have rows reference, querey, and ani
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
db: dataframe
|
|
718
|
+
'''
|
|
719
|
+
|
|
720
|
+
# Vectorized: this used to iterrows() into a string-keyed dict, which took
|
|
721
|
+
# hours and hundreds of GB once a single primary cluster held tens of
|
|
722
|
+
# thousands of genomes (issue #308). Each (reference, querry) pair is encoded
|
|
723
|
+
# as one integer so its reverse can be looked up in bulk.
|
|
724
|
+
logging.debug('averaging reciprocal ANI values')
|
|
725
|
+
ref = db['reference'].values
|
|
726
|
+
qry = db['querry'].values
|
|
727
|
+
names = pd.Index(pd.unique(np.concatenate([ref, qry])))
|
|
728
|
+
n = len(names)
|
|
729
|
+
r = names.get_indexer(ref).astype(np.int64)
|
|
730
|
+
q = names.get_indexer(qry).astype(np.int64)
|
|
731
|
+
|
|
732
|
+
# When a pair appears more than once the last value wins, as before
|
|
733
|
+
ani = pd.Series(db['ani'].values, index=r * n + q)
|
|
734
|
+
ani = ani[~ani.index.duplicated(keep='last')]
|
|
735
|
+
forward = ani.reindex(r * n + q).values
|
|
736
|
+
reverse = ani.reindex(q * n + r).values
|
|
737
|
+
|
|
738
|
+
same = r == q
|
|
739
|
+
missing = ~np.isin(q * n + r, ani.index.values) & ~same
|
|
740
|
+
if missing.any():
|
|
741
|
+
i = np.argmax(missing)
|
|
742
|
+
raise KeyError("{0}-vs-{1}".format(ref[i], qry[i]))
|
|
743
|
+
|
|
744
|
+
db['av_ani'] = np.where(same, 1, (forward + reverse) / 2)
|
|
745
|
+
|
|
746
|
+
logging.debug('averageing done')
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _nucmer_preset(preset):
|
|
750
|
+
'''
|
|
751
|
+
Return the values from a nucmer 'preset'
|
|
752
|
+
|
|
753
|
+
Args:
|
|
754
|
+
preset: either "tight" or "normal"
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
list: [c, n_maxgap, n_noextend, n_method]
|
|
758
|
+
'''
|
|
759
|
+
assert preset in ['tight','normal']
|
|
760
|
+
|
|
761
|
+
if preset == 'tight':
|
|
762
|
+
return 65, 1, True, 'mum'
|
|
763
|
+
|
|
764
|
+
elif preset == 'normal':
|
|
765
|
+
return 65, 90, False, 'mum'
|