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.
@@ -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'