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,636 @@
1
+ import glob
2
+ import logging
3
+ import os
4
+ import sys
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import scipy.cluster
9
+ from scipy.spatial import distance as ssd
10
+
11
+ import drep
12
+ import drep.d_cluster.cluster_utils
13
+ import drep.d_cluster.external
14
+ import drep.d_cluster.utils
15
+ import drep.d_cluster.greedy_clustering
16
+ import drep.d_cluster.union_find
17
+
18
+ class genomeChunk():
19
+ """
20
+ This is an object that just holds stuff related to a chunk of genomes for Mash clustering
21
+ """
22
+
23
+ def __init__(self, locations, number, sketch_folder, genome_names, no_create=False):
24
+ '''
25
+ Initialize this genome chunk
26
+ '''
27
+ self.name = "chunk_{0}".format(number)
28
+ self.genome_locations = locations
29
+ self.chunk_folder = os.path.join(sketch_folder, self.name)
30
+ self.genome_names = genome_names
31
+
32
+ if not no_create:
33
+ if not os.path.exists(self.chunk_folder):
34
+ os.makedirs(self.chunk_folder)
35
+
36
+ def gen_sketch_cmds(self, mash_exe, MASH_s):
37
+ cmds = []
38
+ for location, name in zip(self.genome_locations, self.genome_names):
39
+ file = os.path.join(self.chunk_folder, name)
40
+ if not os.path.isfile(file + '.msh'):
41
+ cmd = [mash_exe, 'sketch', location, '-s', str(MASH_s), '-o',
42
+ file]
43
+ cmds.append(cmd)
44
+ return cmds
45
+
46
+ def gen_paste_cmd(self, mash_exe):
47
+ all_file = os.path.join(self.chunk_folder, 'chunk_all.msh')
48
+ list_file = os.path.join(self.chunk_folder, 'sketch_list.txt')
49
+ with open(list_file, 'w') as f:
50
+ for path in glob.glob(os.path.join(self.chunk_folder, '*.msh')):
51
+ f.write(path + '\n')
52
+ cmd = [mash_exe, 'paste', '-l', all_file, list_file]
53
+ self.all_file = all_file
54
+ return cmd
55
+
56
+ def gen_dist_cmd(self, mash_exe, mash_folder, p):
57
+ dist_file = os.path.join(mash_folder, '{0}_MASH_table.tsv'.format(self.name))
58
+ self.dist_file = dist_file
59
+ cmd = [mash_exe, 'dist', '-p', str(p), self.all_file, self.all_file, '>', dist_file]
60
+ cmd = ' '.join(cmd)
61
+
62
+ return cmd
63
+
64
+ def load_mash_table(self):
65
+ Mdb = drep.d_cluster.utils.parse_mash_table(self.dist_file)
66
+
67
+ # Filter out those genomes that are in the MASH folder but shouldn't be in Mdb
68
+ gs = set(self.genome_names)
69
+ Mdb = Mdb[Mdb['genome1'].isin(gs)]
70
+ Mdb = Mdb[Mdb['genome2'].isin(gs)]
71
+
72
+ # Reorder categories to be correct
73
+ for g in ['genome1', 'genome2']:
74
+ Mdb[g] = Mdb[g].cat.remove_unused_categories()
75
+ Mdb[g] = Mdb[g].cat.reorder_categories(sorted((Mdb[g].unique())), ordered=True)
76
+
77
+ self.Mdb = Mdb
78
+
79
+ def cluster_mash_table(self, **kwargs):
80
+ if len(self.Mdb) > 1:
81
+ Cdb, cluster_ret = cluster_mash_database(self.Mdb, **kwargs)
82
+ self.Cdb = Cdb
83
+ else:
84
+ self.Cdb = pd.DataFrame({'primary_cluster':[1], 'genome':[self.Mdb['genome1'].tolist()[0]]})
85
+
86
+ def get_winning_genomes(self, g2l, **kwargs):
87
+ Cdb = self.Cdb
88
+ Cdb['length'] = Cdb['genome'].map(g2l)
89
+ return self.Cdb.sort_values('length').drop_duplicates(subset=['primary_cluster'], keep='last')['genome'].tolist()
90
+
91
+ def all_vs_all_MASH(Bdb, data_folder, **kwargs):
92
+ """
93
+ Run MASH pairwise within all samples in Bdb
94
+
95
+ Args:
96
+ Bdb: dataframe with genome, location
97
+ data_folder: location to store temporary output files
98
+
99
+ Keyword Args:
100
+ MASH_sketch: size of mash sketches
101
+ dry: dont actually run anything
102
+ processors: number of processors to multithread with
103
+ mash_exe: location of mash excutible (will try and find with shutil if not provided)
104
+ groupSize: max number of mash sketches to hold in each folder
105
+ debug: if True, log all of the commands
106
+ wd: if you want to log commands, you also need the wd
107
+ """
108
+ # Warn if sketch size may be too small for the requested P_ani threshold
109
+ MASH_s = kwargs.get('MASH_sketch', 1000)
110
+ P_ani = kwargs.get('P_ani', 0.9)
111
+ if P_ani < 0.90 and MASH_s <= 1000:
112
+ logging.warning(
113
+ f"P_ani is set to {P_ani} but MASH_sketch is only {MASH_s}. "
114
+ "At low ANI thresholds, a small sketch size can cause Mash to underestimate "
115
+ "distances and place related genomes into separate primary clusters, which "
116
+ "means they will never be compared with the secondary algorithm. "
117
+ "Consider increasing -ms (MASH_sketch) to 10000 or higher."
118
+ )
119
+
120
+ # Set up the mash folder structure
121
+ logdir, MASH_folder, sketch_folder, mash_exe = prepare_mash(data_folder, **kwargs)
122
+
123
+ # Set up chunks of genomes
124
+ genome_chunks = prepare_genome_chunks(Bdb, sketch_folder, MASH_folder, **kwargs)
125
+ if len(genome_chunks) > 1:
126
+ logging.info(f" Will split genomes into {len(genome_chunks)} groups for primary clustering")
127
+
128
+ # Process the chunks individually
129
+ genome_chunks = run_mash_on_genome_chunks(genome_chunks, mash_exe, sketch_folder, MASH_folder, logdir, **kwargs)
130
+
131
+ # If there's only one chunk, we're done here
132
+ if len(genome_chunks) == 1:
133
+ Mdb = genome_chunks[0].Mdb
134
+ Cdb, cluster_ret = cluster_mash_database(Mdb, **kwargs)
135
+ return Mdb, Cdb, cluster_ret
136
+
137
+ # If there's multiple chunks, run a second round
138
+ logging.info(" Final step: comparing between all groups")
139
+ return run_second_round_clustering(Bdb, genome_chunks, data_folder, verbose=True, **kwargs)
140
+
141
+
142
+ def all_vs_all_primary(Bdb, data_folder, **kwargs):
143
+ """
144
+ Dispatch primary clustering to the requested algorithm.
145
+
146
+ 'skani' (default) uses `skani triangle --sparse` + union-find, which never
147
+ builds the N^2 matrix on disk or in RAM and lets a skani --S_algorithm reuse
148
+ the comparisons. 'MASH' is the classic all-vs-all Mash path (pre-v4).
149
+
150
+ Returns (Mdb, Cdb, cluster_ret), matching all_vs_all_MASH.
151
+ """
152
+ method = kwargs.get('primary_algorithm', 'skani')
153
+ if method == 'skani':
154
+ # These only mean something on the MASH path. skani's sparse output never
155
+ # builds the N^2 table they exist to work around, so say so rather than
156
+ # silently ignoring them.
157
+ if kwargs.get('multiround_primary_clustering', False):
158
+ logging.warning(
159
+ "--multiround_primary_clustering only applies to --primary_algorithm MASH "
160
+ "and is ignored with skani. skani's sparse output never builds the full "
161
+ "N^2 table that multiround exists to avoid, and it does not suffer the "
162
+ "chunk-splitting imprecision of multiround.")
163
+ return primary_cluster_skani_sparse(Bdb, data_folder, **kwargs)
164
+ return all_vs_all_MASH(Bdb, data_folder, **kwargs)
165
+
166
+
167
+ def primary_cluster_skani_sparse(Bdb, data_folder, **kwargs):
168
+ """
169
+ Primary clustering via one `skani triangle --sparse` pass + union-find.
170
+
171
+ Only above-screen pairs are ever produced, so there is no N^2 matrix on disk
172
+ or in RAM. Always single-linkage (connected components);
173
+ --classic_primary_clustering / non-single primary_clusterAlg do not apply.
174
+
175
+ The returned Mdb holds *every* edge from that pass, not just the ones above
176
+ P_ani. That is deliberate: secondary clustering compares genomes within a
177
+ primary cluster, which is a subset of what this pass already computed, so it
178
+ can reuse these edges instead of re-running skani per cluster. See
179
+ secondary_clustering_from_primary_edges.
180
+ """
181
+ P_ani = kwargs.get('P_ani', 0.9)
182
+ ani_threshold = P_ani * 100.0
183
+
184
+ # Screen a few points below the ANI threshold so skani's k-mer pre-filter
185
+ # doesn't drop a pair whose full ANI would clear the threshold. Also stay at
186
+ # or below the secondary threshold, since secondary reuses these edges.
187
+ S_ani = kwargs.get('S_ani', 0.95)
188
+ default_screen = max(1.0, min(ani_threshold - 5.0, S_ani * 100.0 - 5.0, 99.0))
189
+ screen = kwargs.get('primary_skani_screen', default_screen)
190
+
191
+ # Minimum percent of a genome that must align for a pair to be reported.
192
+ # Do not lower this casually: skani's ANI ignores how much of the genome
193
+ # aligned, so without this filter genomes sharing only a small conserved
194
+ # region become edges and single linkage chains them into one huge cluster.
195
+ # See run_skani_triangle_sparse for the measurements behind the default.
196
+ min_af = kwargs.get('primary_skani_min_af', 15)
197
+
198
+ # Secondary applies its own coverage filter at cov_thresh, so the single pass
199
+ # has to emit anything secondary might still care about. Ask skani for the
200
+ # looser of the two and apply the stricter primary filter ourselves below.
201
+ cov_thresh = float(kwargs.get('cov_thresh', 0.1))
202
+ emit_min_af = min(min_af, cov_thresh * 100.0)
203
+
204
+ skani_folder = os.path.join(data_folder, 'skani_sparse_files/')
205
+ genome_list = list(Bdb['location'].unique())
206
+
207
+ logging.info(f" Running sparse skani primary clustering on {len(genome_list):,} genomes "
208
+ f"(ANI threshold {ani_threshold:.1f}%, screen {screen:.1f}%, "
209
+ f"min-af {min_af}%, emitting min-af {emit_min_af:.1f}%)")
210
+ sparse_file = drep.d_cluster.external.run_skani_triangle_sparse(
211
+ genome_list, skani_folder, screen, min_af=emit_min_af, **kwargs)
212
+
213
+ all_genomes = list(Bdb['genome'].unique())
214
+ edges = drep.d_cluster.union_find.load_skani_sparse_edges(sparse_file)
215
+ Cdb, stats = drep.d_cluster.union_find.cluster_edges(
216
+ edges, P_ani, all_genomes, cov_threshold=min_af / 100.0)
217
+
218
+ logging.info(f" Sparse skani primary clustering: {stats['edges_kept']:,} edges above "
219
+ f"threshold, {stats['primary_clusters']:,} primary clusters "
220
+ f"({stats['edges_total']:,} edges retained for secondary)")
221
+
222
+ # Mdb keeps every edge so secondary can reuse them. similarity/dist mirror the
223
+ # MASH Mdb schema; alignment_coverage is the aligned fraction of genome1.
224
+ Mdb = edges.rename(columns={'ani': 'similarity'}).copy()
225
+ Mdb['dist'] = 1 - Mdb['similarity']
226
+
227
+ # The sparse path builds no dense matrix, so there is normally no scipy
228
+ # linkage to draw a primary dendrogram from. For modest genome sets the dense
229
+ # matrix is cheap, so build it from the edges purely so the dendrogram still
230
+ # works. Above the cutoff we store a marker and plotting skips it.
231
+ linkage = 'union_find_streaming'
232
+ linkage_db = None
233
+ dendro_max = kwargs.get('primary_dendrogram_max_genomes', 2000)
234
+ if len(all_genomes) <= dendro_max:
235
+ try:
236
+ linkage_db = drep.d_cluster.union_find.edges_to_dense_dist(edges, all_genomes)
237
+ arr = ssd.squareform(np.asarray(linkage_db), checks=False)
238
+ linkage = scipy.cluster.hierarchy.linkage(arr, method='single')
239
+ except Exception as e:
240
+ logging.debug(f"Skipping primary dendrogram linkage computation: {e}")
241
+ linkage = 'union_find_streaming'
242
+ linkage_db = None
243
+
244
+ arguments = {'linkage_method': 'single', 'linkage_cutoff': 1 - P_ani,
245
+ 'comparison_algorithm': 'skani'}
246
+ cluster_ret = [linkage, linkage_db, arguments]
247
+ return Mdb, Cdb, cluster_ret
248
+
249
+
250
+ def secondary_clustering_from_primary_edges(Bdb, Cdb, Mdb, **kwargs):
251
+ """
252
+ Secondary clustering that reuses primary's skani edges instead of re-running
253
+ skani once per primary cluster.
254
+
255
+ The per-cluster comparisons dRep normally runs here recompute ANI values that
256
+ the single sparse pass already produced exactly -- on 10,000 UHGG genomes,
257
+ 94% of the pairs driving secondary clustering were already present, with
258
+ identical ANI to 6 decimal places, and the reused path reproduced the
259
+ two-stage partition exactly (1,232 clusters) in 24s instead of 15 minutes.
260
+
261
+ Returns (Ndb, Cdb, c2ret), matching secondary_clustering.
262
+ """
263
+ edges = Mdb.rename(columns={'similarity': 'ani'})[
264
+ ['genome1', 'genome2', 'ani', 'alignment_coverage']]
265
+ Ndb = drep.d_cluster.union_find.build_ndb_from_edges(edges, Cdb)
266
+
267
+ logging.info(f" Reusing {len(edges):,} primary skani edges for secondary clustering "
268
+ f"(no new comparisons); Ndb has {len(Ndb):,} rows")
269
+
270
+ Cdb2, c2ret = drep.d_cluster.utils._cluster_Ndb(Ndb, comp_method='skani', **kwargs)
271
+ return Ndb, Cdb2, c2ret
272
+
273
+ def prepare_mash(data_folder, **kwargs):
274
+ """
275
+ Make some folders and things
276
+ """
277
+ append = kwargs.get('v2', '')
278
+
279
+ # set up logdir
280
+ if ('wd' in kwargs) and (kwargs.get('debug', False) == True):
281
+ logdir = kwargs.get('wd').get_dir('cmd_logs')
282
+ else:
283
+ logdir = False
284
+
285
+ # Find mash excutable
286
+ mash_exe = kwargs.get('exe_loc', None)
287
+ if mash_exe == None:
288
+ mash_exe = drep.get_exe('mash')
289
+
290
+ # Make a folder to hold this information
291
+ MASH_folder = os.path.join(data_folder, 'MASH_files{0}/'.format(append))
292
+ if not os.path.exists(MASH_folder):
293
+ os.makedirs(MASH_folder)
294
+
295
+ # Make a folder in there to store sketches
296
+ sketch_folder = os.path.join(MASH_folder, 'sketches{0}/'.format(append))
297
+ if not os.path.exists(sketch_folder):
298
+ os.makedirs(sketch_folder)
299
+
300
+ return logdir, MASH_folder, sketch_folder, mash_exe
301
+
302
+ def prepare_genome_chunks(Bdb, sketch_folder, MASH_folder, **kwargs):
303
+ groupSize = kwargs.get('primary_chunksize', 5000)
304
+ l2g = Bdb.set_index('location')['genome'].to_dict()
305
+
306
+ locations = list(Bdb['location'].unique())
307
+ chunks = [locations[x:x + groupSize] for x in range(0, len(locations), groupSize)]
308
+
309
+ genome_chunks = []
310
+ for i, chunk in enumerate(chunks):
311
+ genome_chunks.append(genomeChunk(chunk, i, sketch_folder, [l2g[l] for l in chunk]))
312
+
313
+ return genome_chunks
314
+
315
+ def run_mash_on_genome_chunks(genome_chunks, mash_exe, sketch_folder, MASH_folder, logdir, **kwargs):
316
+ dry = kwargs.get('dry', False)
317
+ p = kwargs.get('processors', 6)
318
+ MASH_s = kwargs.get('MASH_sketch', 1000)
319
+ multi_round = kwargs.get('multiround_primary_clustering', True)
320
+
321
+ # Step 1) Create Mash sketches
322
+ cmds = []
323
+ for GC in genome_chunks:
324
+ cmds += GC.gen_sketch_cmds(mash_exe, MASH_s)
325
+ if (not dry) & (len(cmds) > 0):
326
+ drep.thread_cmds(cmds, logdir=logdir, t=int(p))
327
+
328
+ # Step 2) Combine MASH sketches within chunks
329
+ cmds = [GC.gen_paste_cmd(mash_exe) for GC in genome_chunks]
330
+ if (not dry) & (len(cmds) > 0):
331
+ drep.thread_cmds(cmds, logdir=logdir, t=int(p))
332
+
333
+ # Merge the pasted chunks and make a new genomeChunk if thats what you want
334
+ if (not multi_round) & (len(genome_chunks) > 1):
335
+ cmd, new_gc = drep.d_cluster.utils.merge_genome_chunks(mash_exe, genome_chunks, sketch_folder, MASH_folder)
336
+ genome_chunks = [new_gc]
337
+ drep.run_cmd(cmd, dry, shell=False, logdir=logdir)
338
+
339
+ # Step 3) Run Mash on each chunk
340
+ cmds = [GC.gen_dist_cmd(mash_exe, MASH_folder, p) for GC in genome_chunks]
341
+ for j, cmd in enumerate(cmds):
342
+ if not dry:
343
+ if len(cmds) > 1:
344
+ logging.info(f" Comparing group {j+1} of {len(cmds)}")
345
+ drep.run_cmd(cmd, dry, shell=True, logdir=logdir)
346
+
347
+ # Step 4) Load the Mash tables of each chunk
348
+ for GC in genome_chunks:
349
+ GC.load_mash_table()
350
+
351
+ return genome_chunks
352
+
353
+ def _subsample_mdb(mdb, max_rows):
354
+ """
355
+ Cap a per-chunk Mdb to at most max_rows rows so multiround primary clustering
356
+ doesn't accumulate an O(N^2) table across all chunks (the source of the
357
+ 43k-genome MemoryError at pd.concat). The full pairwise table is only kept for
358
+ storage/inspection; clustering itself does not use the concatenated Mdb.
359
+ """
360
+ if max_rows is None or len(mdb) <= max_rows:
361
+ return mdb
362
+ return mdb.sample(n=max_rows, random_state=0)
363
+
364
+
365
+ def run_second_round_clustering(Bdb, genome_chunks, data_folder, **kwargs):
366
+ verbose = kwargs.get('verbose', False)
367
+
368
+ # Bound the total number of pairwise rows retained for the stored Mdb across
369
+ # all chunks. Set to 0/None to disable (restores pre-v4 unbounded behavior).
370
+ max_mdb_rows = kwargs.get('max_stored_mdb_rows', 5_000_000)
371
+ per_chunk_cap = None
372
+ if max_mdb_rows:
373
+ per_chunk_cap = max(1, int(max_mdb_rows // (len(genome_chunks) + 1)))
374
+
375
+ kwargs_copy = kwargs.copy()
376
+ kwargs_copy['multiround_primary_clustering'] = False
377
+ kwargs_copy['v2'] = '_v2'
378
+
379
+ mdbs = []
380
+
381
+ # Step 1) Create a merged Cdb file
382
+ dbs = []
383
+ for gc in genome_chunks:
384
+ gc.cluster_mash_table(**kwargs_copy)
385
+ cdb = gc.Cdb
386
+ cdb['subcluster'] = ["{0}_{1}".format(gc.name, x) for x in cdb['primary_cluster']]
387
+ dbs.append(cdb)
388
+
389
+ mdb = gc.Mdb
390
+ mdb['genome_chunk'] = gc.name
391
+ # Subsample before retaining so we never hold all N^2 rows at once
392
+ mdbs.append(_subsample_mdb(mdb, per_chunk_cap))
393
+ # Free the chunk's full table now that its clusters are computed
394
+ gc.Mdb = None
395
+
396
+ Cdb = pd.concat(dbs)
397
+
398
+ # Step 2) Pick winners
399
+ g2l = Bdb.set_index('genome')['length'].to_dict()
400
+ Cdb['length'] = Cdb['genome'].map(g2l)
401
+ second_round_genomes = Cdb.sort_values('length').drop_duplicates(subset=['subcluster'], keep='last')['genome'].tolist()
402
+
403
+ if verbose:
404
+ logging.info(f"Comparing {len(second_round_genomes):,} genomes")
405
+
406
+ # Step 3) Run a second round
407
+ logdir, MASH_folder, sketch_folder, mash_exe = prepare_mash(data_folder, **kwargs_copy)
408
+ genome_chunks = prepare_genome_chunks(Bdb[Bdb['genome'].isin(second_round_genomes)], sketch_folder, MASH_folder, **kwargs_copy)
409
+ genome_chunks = run_mash_on_genome_chunks(genome_chunks, mash_exe, sketch_folder, MASH_folder, logdir, **kwargs_copy)
410
+
411
+ # Step 4) Get results
412
+ assert len(genome_chunks) == 1
413
+
414
+ mdb = genome_chunks[0].Mdb
415
+ mdb['genome_chunk'] = 'v2'
416
+ # Cluster on the full second-round table, but only store a bounded subsample
417
+ Cdb2, cluster_ret = cluster_mash_database(mdb, **kwargs)
418
+
419
+ mdbs.append(_subsample_mdb(mdb, per_chunk_cap))
420
+ Mdb = pd.concat(mdbs).reset_index(drop=True)
421
+ Cdb2['primary_representitive'] = True
422
+
423
+ # Step 5) Merge the new Cdb back in with the old
424
+ del Cdb['primary_cluster']
425
+ Cdb = pd.merge(Cdb, Cdb2, on='genome', how='outer')
426
+ o2n = Cdb[Cdb['primary_representitive'] == True].set_index('subcluster')['primary_cluster'].to_dict()
427
+ Cdb['primary_cluster'] = Cdb['subcluster'].map(o2n).astype(int)
428
+
429
+ return Mdb, Cdb, cluster_ret
430
+
431
+ def cluster_mash_database(db, **kwargs):
432
+ '''
433
+ From a Mash database, cluster and return Cdb
434
+
435
+ Args:
436
+ db: Mdb (all_vs_all Mash results)
437
+
438
+ Keyword arguments:
439
+ primary_clusterAlg: how to cluster the primary database (default = single).
440
+ 'single' uses the fast streaming union-find algorithm; any other
441
+ method uses the classic dense scipy path.
442
+ clusterAlg: legacy fallback for primary_clusterAlg (default = single)
443
+ P_ani: threshold to cluster at (default = 0.9)
444
+ classic_primary_clustering: force the dense scipy path
445
+
446
+ Returns:
447
+ list: [Cdb, [linkage, linkage_db, arguments]]
448
+ '''
449
+ logging.debug('Clustering MASH database')
450
+
451
+ # Load key words. Primary clustering has its own linkage method
452
+ # (primary_clusterAlg), independent of the secondary clusterAlg. Fall back to
453
+ # clusterAlg for older callers that only pass that.
454
+ P_Lmethod = kwargs.get('primary_clusterAlg') or kwargs.get('clusterAlg', 'single')
455
+ P_Lcutoff = 1 - kwargs.get('P_ani',.9)
456
+ classic = kwargs.get('classic_primary_clustering', False)
457
+
458
+ db['dist'] = 1 - db['similarity']
459
+
460
+ # Single-linkage clustering at a fixed cutoff is identical to connected
461
+ # components. Compute it directly on the long-format table with union-find and
462
+ # skip the O(N^2) dense pivot entirely (issue #259 / the large-N RAM crash).
463
+ # This is the default; --classic_primary_clustering forces the dense path.
464
+ use_union_find = (not classic) and (P_Lmethod == 'single')
465
+ if use_union_find:
466
+ Cdb = drep.d_cluster.union_find.cluster_long_df(db, P_Lcutoff)
467
+
468
+ arguments = {'linkage_method': 'single', 'linkage_cutoff': P_Lcutoff,
469
+ 'comparison_algorithm': 'MASH'}
470
+
471
+ # The streaming path builds no dense matrix, so by default there is no
472
+ # scipy linkage to plot a primary dendrogram from. For modest genome sets
473
+ # the dense pivot is cheap, so compute the single-linkage matrix purely so
474
+ # the dendrogram can still be drawn. Above the cutoff (or if it fails) we
475
+ # store a marker and downstream plotting skips the dendrogram gracefully.
476
+ linkage = 'union_find_streaming'
477
+ linkage_db = None
478
+ dendro_max = kwargs.get('primary_dendrogram_max_genomes', 2000)
479
+ n_genomes = Cdb['genome'].nunique()
480
+ if n_genomes <= dendro_max and 'genome_chunk' not in db.columns:
481
+ try:
482
+ linkage_db = db.pivot(index="genome1", columns="genome2", values="dist")
483
+ arr = ssd.squareform(np.asarray(linkage_db))
484
+ linkage = scipy.cluster.hierarchy.linkage(arr, method='single')
485
+ except Exception as e:
486
+ logging.debug(f"Skipping primary dendrogram linkage computation: {e}")
487
+ linkage = 'union_find_streaming'
488
+ linkage_db = None
489
+
490
+ cluster_ret = [linkage, linkage_db, arguments]
491
+ return Cdb, cluster_ret
492
+
493
+ # Classic dense path (non-single linkage, or --classic_primary_clustering).
494
+ linkage_db = db.pivot(index="genome1", columns="genome2", values="dist")
495
+ Cdb, linkage = drep.d_cluster.cluster_utils.cluster_hierarchical(linkage_db, linkage_method= P_Lmethod, \
496
+ linkage_cutoff= P_Lcutoff)
497
+ Cdb = Cdb.rename(columns={'cluster':'primary_cluster'})
498
+ Cdb['primary_cluster'] = Cdb['primary_cluster'].astype(int)
499
+
500
+ # Preparing clustering for return
501
+ arguments = {'linkage_method':P_Lmethod,'linkage_cutoff':P_Lcutoff,\
502
+ 'comparison_algorithm':'MASH'}
503
+ cluster_ret = [linkage, linkage_db, arguments]
504
+
505
+ return Cdb, cluster_ret
506
+
507
+ def secondary_clustering(Bdb, Cdb, algorithm, data_folder, **kwargs):
508
+
509
+ if kwargs.get('greedy_secondary_clustering', False) != True:
510
+ Ndb = pd.DataFrame()
511
+ for bdb, name in iteratre_clusters(Bdb, Cdb, id='primary_cluster'):
512
+ logging.debug('running cluster {0}'.format(name))
513
+ # logging.debug('total memory - {0:.2f} Mbp'.format(int(process.memory_info().rss)/1000000))
514
+ ndb = compare_genomes(bdb, algorithm, data_folder, **kwargs)
515
+
516
+ if len(ndb) == 0:
517
+ logging.error("CRITICAL ERROR WITH PRIMARY CLUSTER {0}; TRYING AGAIN".format(name))
518
+ ndb = compare_genomes(bdb, algorithm, data_folder, **kwargs)
519
+
520
+ if len(ndb) > 0:
521
+ ndb['primary_cluster'] = name
522
+ Ndb = pd.concat([Ndb, ndb])
523
+ #Ndb = Ndb.append(ndb)
524
+ else:
525
+ logging.error("DOUBLE CRITICAL ERROR AGAIN WITH PRIMARY CLUSTER {0}; SKIPPING".format(name))
526
+
527
+ # Run clustering on Ndb
528
+ Cdb, c2ret = drep.d_cluster.utils._cluster_Ndb(Ndb, comp_method=algorithm, **kwargs)
529
+
530
+ return Ndb, Cdb, c2ret
531
+
532
+ else:
533
+ return drep.d_cluster.greedy_clustering.greedy_secondary_clustering(Bdb, Cdb, algorithm, data_folder, **kwargs)
534
+
535
+
536
+ def iteratre_clusters(Bdb, Cdb, id='MASH_cluster'):
537
+ Bdb = pd.merge(Bdb, Cdb)
538
+ for cluster in Bdb[id].unique():
539
+ d = Bdb[Bdb[id] == cluster]
540
+ yield d, cluster
541
+
542
+ def compare_genomes(bdb, algorithm, data_folder, **kwargs):
543
+ '''
544
+ Compare a list of genomes using the algorithm specified
545
+
546
+ This method takes in bdb (a table with the columns location and genome), runs
547
+ pair-wise comparisons between all genomes in the sample, and returns a table
548
+ with at least the columns 'reference', 'querry', 'ani','coverage', depending
549
+ on what algorithm is called
550
+
551
+ Args:
552
+ bdb: DataFrame with ['genome', 'location'] (drep.d_filter.load_genomes)
553
+ algorithm: options are ANImf, ANIn, gANI
554
+ data_folder: location to store output files
555
+
556
+ Keyword Arguments:
557
+ wd: either this or prod_folder needed for gANI
558
+ prod_folder: either this or wd needed for gANI
559
+
560
+ Return:
561
+ DataFrame: Ndb (['reference', 'querry', 'ani','coverage'])
562
+ '''
563
+ # To handle other versions of this method which passed in a WorkDirectory
564
+ # instead of data_folder string
565
+ if isinstance(data_folder, drep.WorkDirectory.WorkDirectory):
566
+ data_folder = data_folder.get_dir('data')
567
+
568
+ if not kwargs.get('greedy_secondary_clustering', False):
569
+ if algorithm == 'ANImf':
570
+ genome_list = bdb['location'].tolist()
571
+ working_data_folder = os.path.join(data_folder, 'ANImf_files/')
572
+ df = drep.d_cluster.external.run_pairwise_ANImf(genome_list, working_data_folder, **kwargs)
573
+ return df
574
+
575
+ elif algorithm == 'ANIn':
576
+ genome_list = bdb['location'].tolist()
577
+ working_data_folder = os.path.join(data_folder, 'ANIn_files/')
578
+ df = drep.d_cluster.utils.run_pairwise_ANIn(genome_list, working_data_folder, **kwargs)
579
+ return df
580
+
581
+ elif algorithm == 'fastANI':
582
+ genome_list = bdb['location'].tolist()
583
+ working_data_folder = os.path.join(data_folder, 'fastANI_files/')
584
+ df = drep.d_cluster.external.run_pairwise_fastANI(genome_list, working_data_folder, **kwargs)
585
+ return df
586
+
587
+ elif algorithm == 'skani':
588
+ genome_list = bdb['location'].tolist()
589
+ working_data_folder = os.path.join(data_folder, 'skani_files/')
590
+ df = drep.d_cluster.external.run_pairwise_skani(genome_list, working_data_folder, **kwargs)
591
+ return df
592
+
593
+ elif algorithm == 'gANI':
594
+ # Figure out prodigal folder
595
+ wd = kwargs.get('wd', False)
596
+ if not wd:
597
+ prod_folder = kwargs.pop('prod_folder', False)
598
+ assert prod_folder != False
599
+ else:
600
+ prod_folder = wd.get_dir('prodigal')
601
+
602
+ working_data_folder = os.path.join(data_folder, 'gANI_files/')
603
+ df = drep.d_cluster.external.run_pairwise_gANI(bdb, working_data_folder, \
604
+ prod_folder=prod_folder, **kwargs)
605
+ return df
606
+
607
+ elif algorithm == 'goANI':
608
+ # Figure out prodigal folder
609
+ wd = kwargs.get('wd', False)
610
+ if not wd:
611
+ prod_folder = kwargs.pop('prod_folder', False)
612
+ assert prod_folder != False
613
+ else:
614
+ prod_folder = wd.get_dir('prodigal')
615
+
616
+ working_data_folder = os.path.join(data_folder, 'goANI_files/')
617
+ df = drep.d_cluster.external.run_pairwise_goANI(bdb, working_data_folder, \
618
+ prod_folder=prod_folder, **kwargs)
619
+ return df
620
+
621
+ else:
622
+ logging.error("{0} not supported".format(algorithm))
623
+ sys.exit()
624
+
625
+ else:
626
+ SUPPORTED = ['fastANI']
627
+ if algorithm not in SUPPORTED:
628
+ message = f"{algorithm} is not supported for greedy secondary clustering!\nChoose one of the following supported S_algorithm options: {' '.join(SUPPORTED)}"
629
+ logging.error(message)
630
+ print(message)
631
+ raise NameError
632
+
633
+ working_data_folder = os.path.join(data_folder, 'greedy_clustering/')
634
+ return drep.d_cluster.greedy_clustering.compare_genomes_greedy(bdb, algorithm, working_data_folder, **kwargs)
635
+
636
+