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