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
drep/d_filter.py
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
'''
|
|
3
|
+
d_filter - a subset of drep
|
|
4
|
+
|
|
5
|
+
Filter genomes based on genome length or quality. Also can run prodigal and checkM
|
|
6
|
+
'''
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import glob
|
|
10
|
+
import gzip
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import drep
|
|
16
|
+
import drep.WorkDirectory
|
|
17
|
+
import drep.d_cluster
|
|
18
|
+
import drep.d_choose
|
|
19
|
+
import drep.d_bonus
|
|
20
|
+
|
|
21
|
+
def d_filter_wrapper(wd, **kwargs):
|
|
22
|
+
'''
|
|
23
|
+
Controller for the dRep filter operation
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
wd (WorkDirectory): The current workDirectory
|
|
27
|
+
**kwargs: Command line arguments
|
|
28
|
+
|
|
29
|
+
Keyword Args:
|
|
30
|
+
genomes: genomes to filter in .fasta format
|
|
31
|
+
genomeInfo: location of .csv or .tsv file with the columns: ["genome"(basename of .fasta file of that genome), "completeness"(0-100 value for completeness of the genome), "contamination"(0-100 value of the contamination of the genome)]
|
|
32
|
+
|
|
33
|
+
processors: Threads to use with checkM / prodigal
|
|
34
|
+
overwrite: Overwrite existing data in the work folder
|
|
35
|
+
debug: If True, make extra output when running external scripts
|
|
36
|
+
|
|
37
|
+
length: minimum genome length when filtering
|
|
38
|
+
completeness: minimum genome completeness when filtering
|
|
39
|
+
contamination: maximum genome contamination when filtering
|
|
40
|
+
|
|
41
|
+
ignoreGenomeQuality: Don't run checkM or do any quality-based filtering (not recommended)
|
|
42
|
+
checkM_method: Either lineage_wf (more accurate) or taxonomy_wf (faster)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Nothing: stores Bdb.csv, Chdb.csv, and GenomeInfo.csv in the work directory
|
|
46
|
+
'''
|
|
47
|
+
# Load the WorkDirectory.
|
|
48
|
+
logging.debug("Loading work directory in filter")
|
|
49
|
+
workDirectory = drep.WorkDirectory.WorkDirectory(wd)
|
|
50
|
+
logging.debug(str(workDirectory))
|
|
51
|
+
wd = workDirectory
|
|
52
|
+
|
|
53
|
+
# Validate arguments
|
|
54
|
+
logging.debug("Validating filter arguments")
|
|
55
|
+
_validate_arguments(workDirectory, **kwargs)
|
|
56
|
+
|
|
57
|
+
# Get the thing to filter - bdb (columns = genome, location)
|
|
58
|
+
if wd.hasDb('Bdb'):
|
|
59
|
+
logging.info("Will filter Bdb")
|
|
60
|
+
bdb = wd.get_db('Bdb')
|
|
61
|
+
else:
|
|
62
|
+
logging.info("Will filter the genome list")
|
|
63
|
+
bdb = drep.d_cluster.utils.load_genomes(kwargs['genomes'])
|
|
64
|
+
|
|
65
|
+
# Quick sanity check
|
|
66
|
+
sanity_check(bdb)
|
|
67
|
+
|
|
68
|
+
# Calculate the length and N50 of all genomes
|
|
69
|
+
logging.info("Calculating genome info of genomes")
|
|
70
|
+
Gdb = calc_genome_info(bdb['location'].tolist())
|
|
71
|
+
|
|
72
|
+
# Filter by size if need be
|
|
73
|
+
if kwargs.get('length', 0) > 1:
|
|
74
|
+
logging.debug("Filtering genomes by size")
|
|
75
|
+
bdb = _filter_bdb_length(bdb, Gdb, kwargs['length'])
|
|
76
|
+
|
|
77
|
+
# Get comp/con information
|
|
78
|
+
if kwargs.get('ignoreGenomeQuality', False):
|
|
79
|
+
logging.debug("Skipping all quality-based filtering")
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
Gdb = _get_run_genomeInfo(wd, bdb, **kwargs)
|
|
83
|
+
|
|
84
|
+
# Filter
|
|
85
|
+
if not kwargs.get('ignoreGenomeQuality', False):
|
|
86
|
+
logging.debug("Filtering genomes")
|
|
87
|
+
bdb = filter_bdb(bdb, Gdb, **kwargs)
|
|
88
|
+
|
|
89
|
+
# Save Bdb and genomeInfo
|
|
90
|
+
logging.debug("Storing resulting files")
|
|
91
|
+
|
|
92
|
+
workDirectory.store_db(bdb, 'Bdb')
|
|
93
|
+
if not kwargs.get('ignoreGenomeQuality', False):
|
|
94
|
+
workDirectory.store_db(Gdb, 'genomeInfo')
|
|
95
|
+
|
|
96
|
+
def sanity_check(bdb, **kwargs):
|
|
97
|
+
"""
|
|
98
|
+
Make sure there are no duplicate names or anything
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
bdb:
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
# Check for duplicated genome names
|
|
107
|
+
dupped = [g for g, c in bdb['genome'].value_counts().to_dict().items() if c > 1]
|
|
108
|
+
if len(dupped) > 0:
|
|
109
|
+
logging.error("You have duplicate genome basenames! This doesn't work for dRep; each basename needs to be unique. Here are you problem genomes:")
|
|
110
|
+
logging.error(bdb[bdb['genome'].isin(dupped)].sort_values('genome'))
|
|
111
|
+
raise Exception
|
|
112
|
+
|
|
113
|
+
# Report number of input genomes
|
|
114
|
+
logging.info(f"{len(bdb):,} genomes were input to dRep")
|
|
115
|
+
|
|
116
|
+
# Make a suggestion about greedy?
|
|
117
|
+
pc = kwargs.get('primary_chunksize', None)
|
|
118
|
+
if pc is not None:
|
|
119
|
+
mc = kwargs.get('multiround_primary_clustering', False)
|
|
120
|
+
if (len(bdb) > pc) & (mc == False):
|
|
121
|
+
logging.info(f"Hey! You have {len(bdb)} genomes and arent using greedy algorithms. See https://drep.readthedocs.io/en/latest/choosing_parameters.html#using-greedy-algorithms for info on how to make your run faster")
|
|
122
|
+
|
|
123
|
+
# If you mc, try and tell them to turn on tc
|
|
124
|
+
mc = kwargs.get('multiround_primary_clustering', False)
|
|
125
|
+
tc = kwargs.get('run_tertiary_clustering', False)
|
|
126
|
+
if mc and not tc:
|
|
127
|
+
logging.info(f"Hey! You're running multiround_primary_clustering but not run_tertiary_clustering! You should add --run_tertiary_clustering when running with multiround_primary_clustering to avoid weird placement; see https://drep.readthedocs.io/en/latest/choosing_parameters.html#using-greedy-algorithms for more info")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
GENOMEINFO_REQUIRED_COLUMNS = ['genome', 'completeness', 'contamination']
|
|
131
|
+
GENOMEINFO_DELIMITERS = [(',', 'comma'), ('\t', 'tab'), (';', 'semicolon'), ('|', 'pipe')]
|
|
132
|
+
|
|
133
|
+
# Columns that dRep knows what to do with in a genomeInfo table
|
|
134
|
+
GENOMEINFO_KNOWN_COLUMNS = ['genome', 'completeness', 'contamination',
|
|
135
|
+
'strain_heterogeneity', 'length', 'N50',
|
|
136
|
+
'location', 'centrality']
|
|
137
|
+
|
|
138
|
+
# Raw CheckM2 (quality_report.tsv) and CheckM1 (--tab_table) column names
|
|
139
|
+
GENOMEINFO_COLUMN_TRANSLATION = {
|
|
140
|
+
'Name': 'genome', # CheckM2
|
|
141
|
+
'Bin Id': 'genome', # CheckM1
|
|
142
|
+
'Completeness': 'completeness', # both
|
|
143
|
+
'Contamination': 'contamination', # both
|
|
144
|
+
'Strain heterogeneity': 'strain_heterogeneity', # CheckM1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# CheckM2 run with --general / --specific / --allmodels reports these instead
|
|
148
|
+
# of a plain "Completeness" column; the first one present is used
|
|
149
|
+
GENOMEINFO_COMPLETENESS_FALLBACKS = ['Completeness_General', 'Completeness_Specific']
|
|
150
|
+
|
|
151
|
+
def _genomeInfo_translation(columns):
|
|
152
|
+
'''
|
|
153
|
+
Figure out how to rename raw CheckM1 / CheckM2 columns into genomeInfo columns
|
|
154
|
+
|
|
155
|
+
Raw CheckM2 output is a perfectly good genomeInfo file except that its
|
|
156
|
+
columns are named "Name", "Completeness", and "Contamination", so translate
|
|
157
|
+
it rather than making the user do it by hand (issue #305). Columns that are
|
|
158
|
+
already there under the dRep name are never overwritten
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
columns: the columns of the table that was loaded
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
dict: {current column name: dRep column name}
|
|
165
|
+
'''
|
|
166
|
+
columns = list(columns)
|
|
167
|
+
translation = {c: n for c, n in GENOMEINFO_COLUMN_TRANSLATION.items()
|
|
168
|
+
if (c in columns) and (n not in columns)}
|
|
169
|
+
|
|
170
|
+
if ('completeness' not in columns) and ('Completeness' not in columns):
|
|
171
|
+
for c in GENOMEINFO_COMPLETENESS_FALLBACKS:
|
|
172
|
+
if c in columns:
|
|
173
|
+
translation[c] = 'completeness'
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
# Never rename two columns into the same one; first in the file wins
|
|
177
|
+
deduped = {}
|
|
178
|
+
for c in columns:
|
|
179
|
+
n = translation.get(c, None)
|
|
180
|
+
if (n is not None) and (n not in deduped.values()):
|
|
181
|
+
deduped[c] = n
|
|
182
|
+
|
|
183
|
+
return deduped
|
|
184
|
+
|
|
185
|
+
def load_genomeInfo(location):
|
|
186
|
+
'''
|
|
187
|
+
Load a user-provided genomeInfo file, figuring out the format as you go
|
|
188
|
+
|
|
189
|
+
pandas happily parses a .tsv with sep=',' (you just get one mangled column),
|
|
190
|
+
so the delimiter can't be picked based on which read_csv call fails to raise
|
|
191
|
+
an exception. Instead try each delimiter and keep the one that actually
|
|
192
|
+
yields the columns dRep needs (issue #305). Raw CheckM1 / CheckM2 output is
|
|
193
|
+
then translated into genomeInfo columns
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
location: location of the genomeInfo file
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
DataFrame: genomeInfo
|
|
200
|
+
'''
|
|
201
|
+
best = None
|
|
202
|
+
for sep, sep_name in GENOMEINFO_DELIMITERS:
|
|
203
|
+
try:
|
|
204
|
+
db = pd.read_csv(location, sep=sep)
|
|
205
|
+
except Exception:
|
|
206
|
+
continue
|
|
207
|
+
translation = _genomeInfo_translation(db.columns)
|
|
208
|
+
columns = [translation.get(c, c) for c in db.columns]
|
|
209
|
+
found = len([c for c in GENOMEINFO_REQUIRED_COLUMNS if c in columns])
|
|
210
|
+
score = (found, len(db.columns))
|
|
211
|
+
if (best is None) or (score > best[0]):
|
|
212
|
+
best = (score, sep_name, db, translation)
|
|
213
|
+
|
|
214
|
+
if best is None:
|
|
215
|
+
raise Exception("Cannot parse the genomeInfo file {0}; it must be a "
|
|
216
|
+
"comma- or tab-delimited table".format(location))
|
|
217
|
+
|
|
218
|
+
(found, _), sep_name, Idb, translation = best
|
|
219
|
+
logging.debug("Parsed genomeInfo file {0} as {1}-delimited; the columns are "
|
|
220
|
+
"{2}".format(location, sep_name, list(Idb.columns)))
|
|
221
|
+
|
|
222
|
+
# Translate raw CheckM1 / CheckM2 output
|
|
223
|
+
if len(translation) > 0:
|
|
224
|
+
logging.info("Translating the columns of the provided genomeInfo: {0}".format(
|
|
225
|
+
', '.join(['{0} -> {1}'.format(c, n) for c, n in translation.items()])))
|
|
226
|
+
for c in GENOMEINFO_COMPLETENESS_FALLBACKS:
|
|
227
|
+
if translation.get(c, None) == 'completeness':
|
|
228
|
+
logging.warning('This CheckM2 output has no "Completeness" column, '
|
|
229
|
+
'so "{0}" is being used as completeness'.format(c))
|
|
230
|
+
Idb = Idb.rename(columns=translation)
|
|
231
|
+
Idb = Idb[[c for c in GENOMEINFO_KNOWN_COLUMNS if c in Idb.columns]]
|
|
232
|
+
|
|
233
|
+
if found < len(GENOMEINFO_REQUIRED_COLUMNS):
|
|
234
|
+
logging.warning("Could not find the columns {0} in the genomeInfo file "
|
|
235
|
+
"{1}; the best guess of its format is {2}-delimited "
|
|
236
|
+
"with the columns {3}".format(
|
|
237
|
+
GENOMEINFO_REQUIRED_COLUMNS, location, sep_name,
|
|
238
|
+
list(Idb.columns)))
|
|
239
|
+
|
|
240
|
+
return Idb
|
|
241
|
+
|
|
242
|
+
def _get_run_genomeInfo(workDirectory, bdb, **kwargs):
|
|
243
|
+
'''
|
|
244
|
+
Through kwargs and the wd, get genomeInfo
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
wd: workDirectory
|
|
248
|
+
bdb: current bdb
|
|
249
|
+
kwrags: keyword arguments
|
|
250
|
+
|
|
251
|
+
Keyword arguments:
|
|
252
|
+
no_run: if True, don't run any programs and return None if genomeInfo can't be made
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
DataFrame: genomeInfo
|
|
256
|
+
'''
|
|
257
|
+
|
|
258
|
+
if kwargs.get('genomeInfo', None) != None:
|
|
259
|
+
logging.debug("Loading provided genome quality information")
|
|
260
|
+
Idb = load_genomeInfo(kwargs.get('genomeInfo'))
|
|
261
|
+
try:
|
|
262
|
+
Tdb = _validate_genomeInfo(Idb, bdb)
|
|
263
|
+
except IndexError:
|
|
264
|
+
bdb_cleanup = bdb.loc[~(bdb['genome'].isin(Idb['genome']))]
|
|
265
|
+
missing_count = bdb_cleanup.shape[0]
|
|
266
|
+
logging.debug("Missing info on {0} genomes,"
|
|
267
|
+
" running CheckM".format(missing_count))
|
|
268
|
+
Chdb_cleanup = _run_checkM_wrapper(bdb_cleanup,
|
|
269
|
+
workDirectory,
|
|
270
|
+
**kwargs)
|
|
271
|
+
Idb_cleanup = chdb_to_genomeInfo(Chdb_cleanup)
|
|
272
|
+
Idb_combined = pd.concat([Idb, Idb_cleanup])
|
|
273
|
+
Tdb = _validate_genomeInfo(Idb_combined, bdb)
|
|
274
|
+
Gdb = _add_lengthN50(Tdb, bdb)
|
|
275
|
+
|
|
276
|
+
elif workDirectory.hasDb('genomeInfo'):
|
|
277
|
+
logging.debug("Loading genomeInfo.csv from work directory")
|
|
278
|
+
Idb = workDirectory.get_db('genomeInfo')
|
|
279
|
+
Tdb = _validate_genomeInfo(Idb, bdb)
|
|
280
|
+
Gdb = _add_lengthN50(Tdb, bdb)
|
|
281
|
+
|
|
282
|
+
elif workDirectory.hasDb('Chdb'):
|
|
283
|
+
logging.debug("Loading Chdb.csv from work directory")
|
|
284
|
+
Chdb = workDirectory.get_db('Chdb')
|
|
285
|
+
Idb = chdb_to_genomeInfo(Chdb)
|
|
286
|
+
Tdb = _validate_genomeInfo(Idb, bdb)
|
|
287
|
+
Gdb = _add_lengthN50(Tdb, bdb)
|
|
288
|
+
|
|
289
|
+
elif kwargs.get('no_run', False):
|
|
290
|
+
logging.debug("Making basic genomeInfo")
|
|
291
|
+
Gdb = calc_genome_info(bdb['location'].tolist())
|
|
292
|
+
del Gdb['location']
|
|
293
|
+
|
|
294
|
+
else:
|
|
295
|
+
logging.debug("Running CheckM")
|
|
296
|
+
Chdb = _run_checkM_wrapper(bdb, workDirectory, **kwargs)
|
|
297
|
+
Idb = chdb_to_genomeInfo(Chdb)
|
|
298
|
+
Tdb = _validate_genomeInfo(Idb, bdb)
|
|
299
|
+
Gdb = _add_lengthN50(Tdb, bdb)
|
|
300
|
+
|
|
301
|
+
return Gdb
|
|
302
|
+
|
|
303
|
+
def _validate_genomeInfo(Idb, bdb):
|
|
304
|
+
'''
|
|
305
|
+
Validate genomeinfo file; merge in Gdb if needed
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
genomeInfo: DataFrame of genome info
|
|
309
|
+
bdb: DataFrame with genomes to confirm
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
DataFrame: Validated genomeInfo
|
|
313
|
+
'''
|
|
314
|
+
try:
|
|
315
|
+
logging.debug("HERE IS GENOME INFO:")
|
|
316
|
+
logging.debug('\n' + str(Idb.head()))
|
|
317
|
+
logging.debug("There are the columns: {0}".format(list(Idb.columns)))
|
|
318
|
+
except:
|
|
319
|
+
logging.warning(Idb)
|
|
320
|
+
|
|
321
|
+
# Make sure it has required columns
|
|
322
|
+
for r in ['completeness', 'contamination', 'genome']:
|
|
323
|
+
if r not in Idb.columns:
|
|
324
|
+
raise KeyError("{0} missing from GenomeInfo; the columns that are "
|
|
325
|
+
"there are {1}".format(r, list(Idb.columns)))
|
|
326
|
+
|
|
327
|
+
# Make sure correct datatypes
|
|
328
|
+
for r in ['completeness', 'contamination', 'strain_heterogeneity']:
|
|
329
|
+
if r in Idb:
|
|
330
|
+
Idb[r] = Idb[r].astype(float)
|
|
331
|
+
|
|
332
|
+
# See if full path was used
|
|
333
|
+
for genome in list(bdb['genome'].unique()):
|
|
334
|
+
if genome not in Idb['genome'].tolist():
|
|
335
|
+
if os.path.basename(genome) in [os.path.basename(x) for x in Idb['genome']]:
|
|
336
|
+
logging.warning("Provided genome info has full genome path- correcting")
|
|
337
|
+
Idb['location'] = Idb['genome']
|
|
338
|
+
Idb['genome'] = [os.path.basename(x) for x in Idb['location']]
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
# See if the file extension was stripped (this is what CheckM2 does)
|
|
342
|
+
have = set(Idb['genome'].astype(str))
|
|
343
|
+
missing = [g for g in bdb['genome'].unique() if g not in have]
|
|
344
|
+
if len(missing) > 0:
|
|
345
|
+
matches = {}
|
|
346
|
+
for genome in missing:
|
|
347
|
+
base = os.path.splitext(genome)[0]
|
|
348
|
+
if base in have:
|
|
349
|
+
matches.setdefault(base, []).append(genome)
|
|
350
|
+
|
|
351
|
+
# Only correct names that map back to a single genome
|
|
352
|
+
ambiguous = {b: g for b, g in matches.items() if len(g) > 1}
|
|
353
|
+
if len(ambiguous) > 0:
|
|
354
|
+
logging.warning("Provided genome info is missing the file extension on "
|
|
355
|
+
"genomes that differ only by extension, so it can't be "
|
|
356
|
+
"corrected: {0}".format(sorted(ambiguous.keys())))
|
|
357
|
+
translation = {b: g[0] for b, g in matches.items() if len(g) == 1}
|
|
358
|
+
if len(translation) > 0:
|
|
359
|
+
logging.warning("Provided genome info is missing the file extension on "
|
|
360
|
+
"{0} genomes- correcting".format(len(translation)))
|
|
361
|
+
Idb['genome'] = [translation.get(g, g) for g in Idb['genome'].astype(str)]
|
|
362
|
+
|
|
363
|
+
# Make sure it matchs up with bdb
|
|
364
|
+
for genome in list(bdb['genome'].unique()):
|
|
365
|
+
if genome not in Idb['genome'].tolist():
|
|
366
|
+
raise IndexError("{0} missing from GenomeInfo".format(genome))
|
|
367
|
+
|
|
368
|
+
# Throw warnings if you think this is weird
|
|
369
|
+
for r in ['completeness', 'contamination', 'strain_heterogeneity']:
|
|
370
|
+
if r in Idb:
|
|
371
|
+
if (Idb[r].max() <= 1) & (Idb[r].min() > 0):
|
|
372
|
+
logging.warning("GenomeInfo has no values over 1 for {0}".format(r) + \
|
|
373
|
+
"- these should be 0-100, not 0-1!")
|
|
374
|
+
|
|
375
|
+
return Idb
|
|
376
|
+
|
|
377
|
+
def _add_lengthN50(Idb, bdb):
|
|
378
|
+
'''
|
|
379
|
+
If Gdb doesn't have length or N50, add it
|
|
380
|
+
'''
|
|
381
|
+
# Figure out if you need to add them
|
|
382
|
+
add = False
|
|
383
|
+
for r in ['length', 'N50']:
|
|
384
|
+
if r not in Idb:
|
|
385
|
+
add = True
|
|
386
|
+
|
|
387
|
+
if not add:
|
|
388
|
+
return Idb
|
|
389
|
+
|
|
390
|
+
Gdb = calc_genome_info(bdb['location'].tolist())
|
|
391
|
+
for r in ['length', 'N50']:
|
|
392
|
+
if r not in Idb:
|
|
393
|
+
Idb[r] = Idb['genome'].map(Gdb.set_index('genome')[r].to_dict())
|
|
394
|
+
|
|
395
|
+
return Idb
|
|
396
|
+
|
|
397
|
+
def calc_genome_info(genomes: list):
|
|
398
|
+
'''
|
|
399
|
+
Calculate the length and N50 of a list of genome locations
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
genomes: list of locations of genomes
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
DataFrame: pandas dataframe with ["location", "length", "N50", "genome"]
|
|
406
|
+
'''
|
|
407
|
+
table = {}
|
|
408
|
+
for att in ['location', 'length', 'N50', 'genome']:
|
|
409
|
+
table[att] = []
|
|
410
|
+
|
|
411
|
+
for loc in genomes:
|
|
412
|
+
table['location'].append(loc)
|
|
413
|
+
table['genome'].append(os.path.basename(loc))
|
|
414
|
+
table['length'].append(calc_fasta_length(loc))
|
|
415
|
+
table['N50'].append(calc_n50(loc))
|
|
416
|
+
|
|
417
|
+
return pd.DataFrame(table)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def filter_bdb(bdb, Gdb, **kwargs):
|
|
421
|
+
'''
|
|
422
|
+
Filter bdb based on Gdb
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
bdb: DataFrame with ["genome"]
|
|
426
|
+
Gdb: DataFrame with ["genome", "completeness", "contamination"]
|
|
427
|
+
|
|
428
|
+
Keyword args:
|
|
429
|
+
min_comp: Minimum genome completeness (%)
|
|
430
|
+
max_con: Maximum genome contamination (%)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
DataFrame: bdb filtered based on completeness and contamination
|
|
434
|
+
'''
|
|
435
|
+
# Get kwargs set up
|
|
436
|
+
min_comp = kwargs.get('completeness',False)
|
|
437
|
+
max_con = kwargs.get('contamination',False)
|
|
438
|
+
|
|
439
|
+
# log the number of staring genomes
|
|
440
|
+
start_genomes = list(bdb['genome'].unique())
|
|
441
|
+
if len(start_genomes) <= 0:
|
|
442
|
+
raise ValueError('No genomes')
|
|
443
|
+
|
|
444
|
+
# Filter Gdb based on the required metrics
|
|
445
|
+
db = Gdb.copy()
|
|
446
|
+
if min_comp != False:
|
|
447
|
+
db = db[(db['completeness'] >= min_comp)]
|
|
448
|
+
if max_con != False:
|
|
449
|
+
db = db[db['contamination'] <= max_con]
|
|
450
|
+
|
|
451
|
+
# Make a list of the remaining genomes
|
|
452
|
+
keep_genomes = list(db['genome'].unique())
|
|
453
|
+
|
|
454
|
+
# filter bdb according to these genomes
|
|
455
|
+
bdb = bdb[bdb['genome'].isin(keep_genomes)]
|
|
456
|
+
|
|
457
|
+
logging.info("{0:.2f}% of genomes passed checkM filtering".format(\
|
|
458
|
+
(len(keep_genomes)/len(start_genomes))*100))
|
|
459
|
+
return bdb
|
|
460
|
+
|
|
461
|
+
def _filter_bdb_length(bdb, Gdb, min_length):
|
|
462
|
+
'''
|
|
463
|
+
Filter bdb (['genome', 'location']) based on the minimum length in Gdb
|
|
464
|
+
(['location', 'length', 'N50'])
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
bdb: DataFrame
|
|
468
|
+
Gdb: DataFrame
|
|
469
|
+
min_length: length to filter on
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
DataFrame: bdb filtered by length
|
|
473
|
+
'''
|
|
474
|
+
|
|
475
|
+
start = len(bdb['location'].unique())
|
|
476
|
+
bdb['length'] = bdb['location'].map(Gdb.set_index('location')['length'].to_dict())
|
|
477
|
+
x = bdb[bdb['length'] >= min_length]
|
|
478
|
+
end = len(x['location'].unique())
|
|
479
|
+
|
|
480
|
+
logging.info("{0:.2f}% of genomes passed length filtering".format((end/start)*100))
|
|
481
|
+
|
|
482
|
+
return x
|
|
483
|
+
|
|
484
|
+
def validate_chdb(Chdb, bdb):
|
|
485
|
+
'''
|
|
486
|
+
Make sure all genomes in bdb are in Chdb
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
Chdb: dataframe of checkM information
|
|
490
|
+
bdb: dataframe with ['genome']
|
|
491
|
+
'''
|
|
492
|
+
quit = False
|
|
493
|
+
b_genomes = bdb['genome'].tolist()
|
|
494
|
+
for genome in b_genomes:
|
|
495
|
+
if genome not in Chdb['Bin Id'].tolist():
|
|
496
|
+
logging.error("{0} is not in checkM db".format(genome))
|
|
497
|
+
quit = True
|
|
498
|
+
if quit:
|
|
499
|
+
logging.error("New checkM db needs to be made\nSee https://drep.readthedocs.io/en/latest/advanced_use.html#troubleshooting-checkm for help troubleshooting")
|
|
500
|
+
sys.exit()
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
def _validate_arguments(wd,**kwargs):
|
|
504
|
+
'''
|
|
505
|
+
Validate arguments to the filter wrapper
|
|
506
|
+
|
|
507
|
+
1) Make sure you either have a genome list or Bdb to filter. Bdb is the rarer
|
|
508
|
+
case, but can be done. Throw warnings if needed.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
wd (WorkDirectory): The current workDirectory
|
|
512
|
+
**kwargs: Command line arguments
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Nothing
|
|
516
|
+
'''
|
|
517
|
+
|
|
518
|
+
# Figure out what you're going to filter
|
|
519
|
+
if wd.hasDb('Wdb'):
|
|
520
|
+
logging.warning("NOTE: Wdb already exists! This will not be filtered! Be sure you know what you're doing")
|
|
521
|
+
if wd.hasDb('Bdb'):
|
|
522
|
+
if kwargs.get('genomes',None) != None:
|
|
523
|
+
logging.error("Both Bdb and a genome list are found- either don't include "\
|
|
524
|
+
+ "a genome list or start a new work directory!")
|
|
525
|
+
sys.exit()
|
|
526
|
+
if wd.hasDb('Cdb'):
|
|
527
|
+
logging.warning("NOTE: Clustering already exists! This will not be filtered! Be sure you know what you're doing")
|
|
528
|
+
else:
|
|
529
|
+
if kwargs.get('genomes',None) == None:
|
|
530
|
+
logging.error("I don't have anything to filter! Give me a genome list")
|
|
531
|
+
sys.exit()
|
|
532
|
+
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
def _run_checkM_wrapper(bdb, workDirectory, **kwargs):
|
|
536
|
+
'''
|
|
537
|
+
Controller for running checkM through a workDirectory
|
|
538
|
+
'''
|
|
539
|
+
# Run prodigal
|
|
540
|
+
prod_folder = workDirectory.get_dir('prodigal')
|
|
541
|
+
logging.info("Running prodigal")
|
|
542
|
+
run_prodigal(bdb['location'].tolist(), prod_folder, wd=workDirectory, **kwargs)
|
|
543
|
+
|
|
544
|
+
# Make the folder to house the checkM info
|
|
545
|
+
checkM_loc = workDirectory.get_dir('checkM')
|
|
546
|
+
|
|
547
|
+
# Run checkM
|
|
548
|
+
logging.info("Running checkM")
|
|
549
|
+
checkM_outfolder = checkM_loc + 'checkM_outdir/'
|
|
550
|
+
Chdb = run_checkM(prod_folder, checkM_outfolder, wd=workDirectory, **kwargs)
|
|
551
|
+
validate_chdb(Chdb, bdb)
|
|
552
|
+
|
|
553
|
+
# Fix genome size and N50 of Chdb
|
|
554
|
+
Chdb = _fix_chdb(Chdb,bdb)
|
|
555
|
+
|
|
556
|
+
# Save checkM run
|
|
557
|
+
workDirectory.store_db(Chdb,'Chdb')
|
|
558
|
+
|
|
559
|
+
return Chdb
|
|
560
|
+
|
|
561
|
+
def _fix_chdb(Chdb, Bdb):
|
|
562
|
+
'''
|
|
563
|
+
Add corrent genome length and N50 info when running checkM with proteins
|
|
564
|
+
'''
|
|
565
|
+
g2s = {}
|
|
566
|
+
g2n = {}
|
|
567
|
+
for genome in Chdb['Bin Id'].unique():
|
|
568
|
+
loc = Bdb['location'][Bdb['genome'] == genome].tolist()[0]
|
|
569
|
+
g2s[genome] = calc_fasta_length(loc)
|
|
570
|
+
g2n[genome] = calc_n50(loc)
|
|
571
|
+
Chdb['Genome size (bp)'] = Chdb['Bin Id'].map(g2s)
|
|
572
|
+
Chdb['N50 (scaffolds)'] = Chdb['Bin Id'].map(g2n)
|
|
573
|
+
|
|
574
|
+
return Chdb
|
|
575
|
+
|
|
576
|
+
def calc_n50(loc):
|
|
577
|
+
'''
|
|
578
|
+
Calculate the N50 of a .fasta file
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
loc: location of .fasta file.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
int: N50 of .fasta file.
|
|
585
|
+
'''
|
|
586
|
+
lengths = []
|
|
587
|
+
is_header = True
|
|
588
|
+
|
|
589
|
+
if loc.endswith('.gz'):
|
|
590
|
+
open_func = gzip.open
|
|
591
|
+
mode = 'rt'
|
|
592
|
+
else:
|
|
593
|
+
open_func = open
|
|
594
|
+
mode = 'r'
|
|
595
|
+
|
|
596
|
+
with open_func(loc, mode) as handle:
|
|
597
|
+
seq_length = 0
|
|
598
|
+
for line in handle:
|
|
599
|
+
if line.startswith('>'):
|
|
600
|
+
if not is_header:
|
|
601
|
+
lengths.append(seq_length)
|
|
602
|
+
seq_length = 0
|
|
603
|
+
is_header = True
|
|
604
|
+
else:
|
|
605
|
+
seq_length += len(line.strip())
|
|
606
|
+
is_header = False
|
|
607
|
+
if not is_header:
|
|
608
|
+
lengths.append(seq_length)
|
|
609
|
+
|
|
610
|
+
half = sum(lengths) / 2
|
|
611
|
+
tally = 0
|
|
612
|
+
for l in sorted(lengths, reverse=True):
|
|
613
|
+
tally += l
|
|
614
|
+
if tally > half:
|
|
615
|
+
return l
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def run_prodigal(genome_list, out_dir, **kwargs):
|
|
619
|
+
'''
|
|
620
|
+
Run prodigal on a set of genomes, store the output in the out_dir
|
|
621
|
+
|
|
622
|
+
Args:
|
|
623
|
+
genome_list: list of genomes to run prodigal on
|
|
624
|
+
out_dir: output directory to store prodigal output
|
|
625
|
+
|
|
626
|
+
Keyword Args:
|
|
627
|
+
processors: number of processors to multithread with
|
|
628
|
+
exe_loc: location of prodigal excutible (will try and find with shutil if not provided)
|
|
629
|
+
debug: log all of the commands
|
|
630
|
+
wd: if you want to log commands, you also need the wd
|
|
631
|
+
|
|
632
|
+
'''
|
|
633
|
+
# Get set up
|
|
634
|
+
t = kwargs.get('processors','6')
|
|
635
|
+
loc = kwargs.get('exe_loc', None)
|
|
636
|
+
if loc == None:
|
|
637
|
+
loc = drep.get_exe('prodigal')
|
|
638
|
+
|
|
639
|
+
# Make sure it's a list
|
|
640
|
+
assert type(genome_list) == list
|
|
641
|
+
|
|
642
|
+
# Make list of commands
|
|
643
|
+
cmds = []
|
|
644
|
+
for genome in genome_list:
|
|
645
|
+
fna = "{0}{1}".format(os.path.join(out_dir,os.path.basename(genome)),'.fna')
|
|
646
|
+
faa = "{0}{1}".format(os.path.join(out_dir,os.path.basename(genome)),'.faa')
|
|
647
|
+
if os.path.exists(fna) and os.path.exists(faa):
|
|
648
|
+
pass
|
|
649
|
+
else:
|
|
650
|
+
cmds.append(['prodigal','-i',genome,'-d',fna,'-a',faa,'-m','-p','single'])
|
|
651
|
+
|
|
652
|
+
# Run commands
|
|
653
|
+
if len(cmds) > 0:
|
|
654
|
+
if ('wd' in kwargs) and (kwargs.get('debug', False) == True):
|
|
655
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
656
|
+
else:
|
|
657
|
+
logdir = False
|
|
658
|
+
#logdir = "/home/mattolm/Programs/drep/tests/test_backend/logs/"
|
|
659
|
+
|
|
660
|
+
drep.thread_cmds(cmds, shell=False, logdir=logdir, t=int(t))
|
|
661
|
+
|
|
662
|
+
else:
|
|
663
|
+
logging.info("Past prodigal runs found- will not re-run")
|
|
664
|
+
|
|
665
|
+
def run_checkM(genome_folder_whole, checkm_outf_whole, **kwargs):
|
|
666
|
+
'''
|
|
667
|
+
Run checkM
|
|
668
|
+
|
|
669
|
+
WARNING- this will result in wrong genome length and genome N50 estimate, due to
|
|
670
|
+
it being run on prodigal output
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
genome_folder: location of folder to run checkM on - should be full of files ending in .faa (result of prodigal)
|
|
674
|
+
checkm_outf: location of folder to store checkM output
|
|
675
|
+
|
|
676
|
+
Keyword args:
|
|
677
|
+
processors: number of threads
|
|
678
|
+
checkm_method: either lineage_wf or taxonomy_wf
|
|
679
|
+
debug: log all of the commands
|
|
680
|
+
wd: if you want to log commands, you also need the wd
|
|
681
|
+
set_recursion: if not 0, set the python recursion
|
|
682
|
+
'''
|
|
683
|
+
# Get set up
|
|
684
|
+
check_exe = _checkm_get_exe()
|
|
685
|
+
t = str(kwargs.get('processors','6'))
|
|
686
|
+
checkm_method = kwargs.get('checkM_method', 'lineage_wf')
|
|
687
|
+
checkm_group_size = kwargs.get('checkm_group_size', 1000)
|
|
688
|
+
|
|
689
|
+
# Set recursion
|
|
690
|
+
R = kwargs.get('set_recursion', '0')
|
|
691
|
+
if R != '0':
|
|
692
|
+
logging.warning('Setting Maximum Recursion depth to {0}'.format(R))
|
|
693
|
+
sys.setrecursionlimit(int(R))
|
|
694
|
+
|
|
695
|
+
# Establish groups
|
|
696
|
+
dbs = []
|
|
697
|
+
for genome_folder, checkm_outf in _iterate_checkm_groups(genome_folder_whole, checkm_outf_whole, checkm_group_size):
|
|
698
|
+
|
|
699
|
+
# Run checkM initial
|
|
700
|
+
if checkm_method == 'taxonomy_wf':
|
|
701
|
+
cmd = [check_exe,checkm_method,'domain','Bacteria',genome_folder,checkm_outf,'-f',\
|
|
702
|
+
checkm_outf + '/results.tsv','--tab_table','-t',str(t),'-g','-x','faa']
|
|
703
|
+
else:
|
|
704
|
+
cmd = [check_exe,checkm_method,genome_folder,checkm_outf,'-f',\
|
|
705
|
+
checkm_outf + '/results.tsv','--tab_table','-t',str(t),'--pplacer_threads',\
|
|
706
|
+
str(min(64, int(t))),'-g','-x','faa']
|
|
707
|
+
|
|
708
|
+
logging.debug("Running CheckM with command: {0}".format(' '.join(cmd)))
|
|
709
|
+
|
|
710
|
+
if ('wd' in kwargs) & (kwargs.get('debug', False) == True):
|
|
711
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
712
|
+
else:
|
|
713
|
+
logdir = False
|
|
714
|
+
drep.run_cmd(cmd, shell=False, logdir=logdir)
|
|
715
|
+
|
|
716
|
+
# Run checkM again for the better table
|
|
717
|
+
if checkm_method == 'taxonomy_wf':
|
|
718
|
+
lineage = checkm_outf + 'Bacteria.ms'
|
|
719
|
+
else:
|
|
720
|
+
lineage = checkm_outf + 'lineage.ms'
|
|
721
|
+
desired_file = checkm_outf + 'Chdb.tsv'
|
|
722
|
+
cmd = [check_exe,'qa', lineage, checkm_outf, '-f', desired_file, '-t',\
|
|
723
|
+
str(t), '--tab_table','-o', '2']
|
|
724
|
+
logging.debug("Running CheckM with command: {0}".format(' '.join(cmd)))
|
|
725
|
+
|
|
726
|
+
if ('wd' in kwargs) & (kwargs.get('debug', False) == True):
|
|
727
|
+
logdir = kwargs.get('wd').get_dir('cmd_logs')
|
|
728
|
+
else:
|
|
729
|
+
logdir = False
|
|
730
|
+
drep.run_cmd(cmd, shell=False, logdir=logdir)
|
|
731
|
+
|
|
732
|
+
# Load table
|
|
733
|
+
try:
|
|
734
|
+
chdb = pd.read_table(desired_file,sep='\t')
|
|
735
|
+
chdb['Bin Id'] = chdb['Bin Id'].astype(str)
|
|
736
|
+
except:
|
|
737
|
+
logging.error("!!! checkM failed !!!\nSee https://drep.readthedocs.io/en/latest/advanced_use.html#troubleshooting-checkm for help troubleshooting")
|
|
738
|
+
sys.exit()
|
|
739
|
+
|
|
740
|
+
# Return table
|
|
741
|
+
dbs.append(chdb)
|
|
742
|
+
|
|
743
|
+
return pd.concat(dbs).reset_index(drop=True)
|
|
744
|
+
|
|
745
|
+
def _checkm_get_exe():
|
|
746
|
+
loc, works = drep.d_bonus.find_program('checkm')
|
|
747
|
+
if loc == None:
|
|
748
|
+
logging.error('Cannot locate the program {0}- make sure its in the system path' \
|
|
749
|
+
.format('checkm'))
|
|
750
|
+
sys.exit()
|
|
751
|
+
if works == False:
|
|
752
|
+
logging.error('Program {0} is not working!! Im going to crash now' \
|
|
753
|
+
.format('checkm'))
|
|
754
|
+
sys.exit()
|
|
755
|
+
return loc
|
|
756
|
+
|
|
757
|
+
def _iterate_checkm_groups(genome_folder_whole, checkm_outf_whole, checkm_group_size):
|
|
758
|
+
"""
|
|
759
|
+
Break up the checkM input into groups of checkm_group_size
|
|
760
|
+
"""
|
|
761
|
+
# Get the number of prodigal files you have
|
|
762
|
+
total_genes = glob.glob(os.path.join(genome_folder_whole, '*faa'))
|
|
763
|
+
chunks = [total_genes[x:x + checkm_group_size] for x in range(0, len(total_genes), checkm_group_size)]
|
|
764
|
+
|
|
765
|
+
if len(chunks) == 1:
|
|
766
|
+
yield genome_folder_whole, checkm_outf_whole
|
|
767
|
+
|
|
768
|
+
else:
|
|
769
|
+
logging.info(f"Running checkM in {len(chunks)} chunks")
|
|
770
|
+
|
|
771
|
+
if not os.path.isdir(checkm_outf_whole):
|
|
772
|
+
os.mkdir(checkm_outf_whole)
|
|
773
|
+
|
|
774
|
+
for i, chunk in enumerate(chunks):
|
|
775
|
+
logging.info(f"Running checkM chunk {i}")
|
|
776
|
+
|
|
777
|
+
new_checkm_out = os.path.join(checkm_outf_whole, f"genome_chunk_{i}_out/")
|
|
778
|
+
new_checkm_prods = os.path.join(checkm_outf_whole, f"genome_chunk_{i}_genes")
|
|
779
|
+
|
|
780
|
+
os.mkdir(new_checkm_out)
|
|
781
|
+
os.mkdir(new_checkm_prods)
|
|
782
|
+
|
|
783
|
+
for faa in chunk:
|
|
784
|
+
os.symlink(faa, os.path.join(new_checkm_prods, os.path.basename(faa)))
|
|
785
|
+
|
|
786
|
+
yield new_checkm_prods, new_checkm_out
|
|
787
|
+
|
|
788
|
+
def calc_fasta_length(fasta_loc):
|
|
789
|
+
'''
|
|
790
|
+
Calculate the length of the .fasta file and return length
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
fasta_loc: location of .fasta file
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
int: total length of all sequences in the .fasta file
|
|
797
|
+
'''
|
|
798
|
+
total = 0
|
|
799
|
+
|
|
800
|
+
if fasta_loc.endswith('.gz'):
|
|
801
|
+
open_func = gzip.open
|
|
802
|
+
mode = 'rt'
|
|
803
|
+
else:
|
|
804
|
+
open_func = open
|
|
805
|
+
mode = 'r'
|
|
806
|
+
|
|
807
|
+
with open_func(fasta_loc, mode) as handle:
|
|
808
|
+
for line in handle:
|
|
809
|
+
if line.startswith('>'):
|
|
810
|
+
pass
|
|
811
|
+
else:
|
|
812
|
+
total += len(line.strip())
|
|
813
|
+
|
|
814
|
+
return total
|
|
815
|
+
|
|
816
|
+
def chdb_to_genomeInfo(chdb):
|
|
817
|
+
'''
|
|
818
|
+
Convert the output of checkM (chdb) into genomeInfo
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
chdb: dataframe of checkM
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
DataFrame: genomeInfo
|
|
825
|
+
'''
|
|
826
|
+
Gdb = chdb.copy()
|
|
827
|
+
Gdb.rename(columns={'Bin Id': 'genome', 'Completeness': 'completeness',\
|
|
828
|
+
'Contamination':'contamination', \
|
|
829
|
+
'Strain heterogeneity':'strain_heterogeneity'}, inplace=True)
|
|
830
|
+
Gdb = Gdb[['genome', 'completeness', 'contamination', 'strain_heterogeneity']]
|
|
831
|
+
return Gdb
|