inStrain 1.11.0__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.
Files changed (46) hide show
  1. inStrain/GeneProfile.py +892 -0
  2. inStrain/SNVprofile.py +1183 -0
  3. inStrain/__init__.py +1 -0
  4. inStrain/_version.py +1 -0
  5. inStrain/argumentParser.py +399 -0
  6. inStrain/compare_controller.py +715 -0
  7. inStrain/compare_greedy.py +134 -0
  8. inStrain/compare_utils.py +285 -0
  9. inStrain/controller.py +536 -0
  10. inStrain/deprecated/DEPRECATEDmapping_plots.py +82 -0
  11. inStrain/deprecated/__init__.py +901 -0
  12. inStrain/deprecated/deprecated_filter_reads.py +491 -0
  13. inStrain/deprecated/deprecated_gene_statistics.py +226 -0
  14. inStrain/deprecated/plottingUtilities.py +1767 -0
  15. inStrain/filter_reads.py +956 -0
  16. inStrain/genomeUtilities.py +981 -0
  17. inStrain/helper_files/NullModel.txt +10000 -0
  18. inStrain/irep_utilities.py +294 -0
  19. inStrain/logUtils.py +1013 -0
  20. inStrain/parse_annotations.py +463 -0
  21. inStrain/plotting/SNV_plots.py +77 -0
  22. inStrain/plotting/__init__.py +18 -0
  23. inStrain/plotting/compare_plots.py +263 -0
  24. inStrain/plotting/gene_plots.py +87 -0
  25. inStrain/plotting/linkage_plots.py +196 -0
  26. inStrain/plotting/mapping_plots.py +276 -0
  27. inStrain/plotting/plotting_controller.py +170 -0
  28. inStrain/plotting/positional_plots.py +569 -0
  29. inStrain/plotting/utilities.py +96 -0
  30. inStrain/polymorpher.py +586 -0
  31. inStrain/profile/__init__.py +18 -0
  32. inStrain/profile/fasta.py +169 -0
  33. inStrain/profile/linkage.py +283 -0
  34. inStrain/profile/profile_controller.py +473 -0
  35. inStrain/profile/profile_utilities.py +905 -0
  36. inStrain/profile/samtools_ops.py +164 -0
  37. inStrain/profile/snv_utilities.py +290 -0
  38. inStrain/quickProfile.py +169 -0
  39. inStrain/readComparer.py +911 -0
  40. inStrain/utils.py +94 -0
  41. instrain-1.11.0.data/scripts/inStrain +31 -0
  42. instrain-1.11.0.dist-info/METADATA +30 -0
  43. instrain-1.11.0.dist-info/RECORD +46 -0
  44. instrain-1.11.0.dist-info/WHEEL +5 -0
  45. instrain-1.11.0.dist-info/licenses/LICENSE +21 -0
  46. instrain-1.11.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,892 @@
1
+ #!/usr/bin/env python
2
+
3
+ import os
4
+ import csv
5
+ import sys
6
+ import time
7
+ import glob
8
+ import logging
9
+ import warnings
10
+ import argparse
11
+ import traceback
12
+ import multiprocessing
13
+
14
+ import Bio
15
+ import numpy as np
16
+ import pandas as pd
17
+ from tqdm import tqdm
18
+ from Bio import SeqIO
19
+ from Bio.Seq import Seq, MutableSeq
20
+ import concurrent.futures
21
+ from concurrent import futures
22
+ from inStrain import SNVprofile
23
+ from collections import defaultdict
24
+
25
+ try:
26
+ from Bio.Data.CodonTable import standard_dna_table as default_codon_table
27
+ except ImportError:
28
+ with warnings.catch_warnings():
29
+ warnings.simplefilter("ignore")
30
+ from Bio.codonalign.codonalphabet import default_codon_table
31
+
32
+ import inStrain.SNVprofile
33
+ import inStrain.controller
34
+ #import inStrain.profileUtilities
35
+ import inStrain.logUtils
36
+
37
+ class Controller():
38
+ '''
39
+ The command line access point to the program
40
+ '''
41
+ def main(self, args):
42
+ '''
43
+ The main method when run on the command line
44
+ '''
45
+ # Parse arguments
46
+ args = self.validate_input(args)
47
+
48
+ vargs = vars(args)
49
+ IS = vargs.pop('IS')
50
+ GF = vargs.pop('gene_file')
51
+
52
+ # Read the genes file
53
+ logging.debug('Loading genes')
54
+ scaff_2_gene_database, scaff2gene2sequence = parse_genes(GF, **vargs)
55
+ GdbP = pd.concat([x for x in scaff_2_gene_database.values()])
56
+
57
+ # Calculate all your parallelized gene-level stuff
58
+ name2result = calculate_gene_metrics(IS, GdbP, scaff2gene2sequence, **vargs)
59
+
60
+ # Store information
61
+ IS.store('genes_fileloc', GF, 'value', 'Location of genes file that was used to call genes')
62
+ IS.store('genes_table', GdbP, 'pandas', 'Location of genes in the associated genes_file')
63
+ IS.store('genes_coverage', name2result['coverage'], 'pandas', 'Coverage of individual genes')
64
+ IS.store('genes_clonality', name2result['clonality'], 'pandas', 'Clonality of individual genes')
65
+ IS.store('genes_SNP_count', name2result['SNP_density'], 'pandas', 'SNP density and counts of individual genes')
66
+ IS.store('SNP_mutation_types', name2result['SNP_mutation_types'], 'pandas', 'The mutation types of SNPs')
67
+
68
+ if vargs.get('store_everything', False):
69
+ IS.store('scaff2gene2sequence', scaff2gene2sequence, 'pickle', 'Dicitonary of scaff -> gene -> nucleotide sequence')
70
+
71
+ # Store the output
72
+ IS.generate('gene_info', **vargs)
73
+ IS.generate("SNVs", **vargs)
74
+
75
+ def validate_input(self, args):
76
+ '''
77
+ Validate and mess with the arguments a bit
78
+ '''
79
+ # Make sure the IS object is OK and load it
80
+ assert os.path.exists(args.IS)
81
+ args.IS = inStrain.SNVprofile.SNVprofile(args.IS)
82
+
83
+ # Set up the logger
84
+ log_loc = args.IS.get_location('log') + 'log.log'
85
+ inStrain.controller.setup_logger(log_loc)
86
+
87
+ return args
88
+
89
+ def gene_profile_worker(gene_cmd_queue, gene_result_queue, single_thread=False):
90
+ '''
91
+ Worker to profile splits
92
+ '''
93
+ while True:
94
+ # Get command
95
+ if not single_thread:
96
+ cmds = gene_cmd_queue.get(True)
97
+ else:
98
+ try:
99
+ cmds = gene_cmd_queue.get(timeout=5)
100
+ except:
101
+ return
102
+
103
+ # Process cmd
104
+ GPs = profile_genes_wrapper(cmds)
105
+ gene_result_queue.put(GPs)
106
+
107
+ def profile_genes_wrapper(cmds):
108
+ '''
109
+ Take a group of commands and run geneprofile
110
+ '''
111
+ results = []
112
+ for cmd in cmds:
113
+ try:
114
+ results.append(profile_genes(cmd.scaffold, **cmd.arguments))
115
+ except Exception as e:
116
+ logging.error("FAILURE GeneException {0}".format(str(cmd.scaffold)))
117
+ logging.error(str(traceback.format_exc()))
118
+ logging.error(str(e))
119
+
120
+ results.append(None)
121
+ return results
122
+
123
+ def calculate_gene_metrics(IS, GdbP, scaff2gene2sequenceP, **kwargs):
124
+ '''
125
+ Calculate the metrics of all genes on a parallelized scaffold-level basis
126
+
127
+ IS = Initialized inStrain.SNVprofile
128
+ GdbP = List of gene locations
129
+ gene2sequenceP = Dicitonary of gene -> nucleotide sequence
130
+ '''
131
+ inStrain.logUtils.log_checkpoint("GeneProfile", "calculate_gene_metrics", "start")
132
+
133
+ # Get key word arguments for the wrapper
134
+ p = int(kwargs.get('processes', 6))
135
+
136
+ # Make a list of scaffolds to profile the genes of
137
+ scaffolds_with_genes = set(GdbP['scaffold'].unique())
138
+ scaffolds_in_IS = set(IS._get_covt_keys())
139
+ scaffolds_to_profile = scaffolds_with_genes.intersection(scaffolds_in_IS)
140
+ logging.info("{0} scaffolds with genes in the input; {1} scaffolds in the IS, {2} to compare".format(
141
+ len(scaffolds_with_genes), len(scaffolds_in_IS), len(scaffolds_to_profile)))
142
+
143
+ # Calculate scaffold -> number of genes to profile
144
+ s2g = GdbP['scaffold'].value_counts().to_dict()
145
+ kwargs['s2g'] = s2g
146
+
147
+ # Make global objects for the profiling
148
+ inStrain.logUtils.log_checkpoint("GeneProfile", "make_globals", "start")
149
+ global CumulativeSNVtable
150
+ CumulativeSNVtable = IS.get('cumulative_snv_table')
151
+ if len(CumulativeSNVtable) > 0:
152
+ CumulativeSNVtable = CumulativeSNVtable.sort_values('mm')
153
+ else:
154
+ CumulativeSNVtable = pd.DataFrame(columns=['scaffold'])
155
+
156
+ global covTs
157
+ covTs = IS.get('covT', scaffolds=scaffolds_to_profile)
158
+
159
+ global clonTs
160
+ clonTs = IS.get('clonT', scaffolds=scaffolds_to_profile)
161
+
162
+ global scaff2gene2sequence
163
+ scaff2gene2sequence = scaff2gene2sequenceP
164
+
165
+ global Gdb
166
+ Gdb = GdbP
167
+ inStrain.logUtils.log_checkpoint("GeneProfile", "make_globals", "end")
168
+
169
+ ctx = multiprocessing.get_context('fork')
170
+
171
+ # Generate commands and queue them
172
+ logging.debug('Creating commands')
173
+ cmd_groups = [x for x in iterate_commands(scaffolds_to_profile, Gdb, kwargs)]
174
+ logging.debug('There are {0} cmd groups'.format(len(cmd_groups)))
175
+
176
+ inStrain.logUtils.log_checkpoint("GeneProfile", "create_queue", "start")
177
+ gene_cmd_queue = ctx.Queue()
178
+ gene_result_queue = ctx.Queue()
179
+ GeneProfiles = []
180
+
181
+ for cmd_group in cmd_groups:
182
+ gene_cmd_queue.put(cmd_group)
183
+
184
+ inStrain.logUtils.log_checkpoint("GeneProfile", "create_queue", "end")
185
+
186
+ if p > 1:
187
+ logging.debug('Establishing processes')
188
+ processes = []
189
+ for i in range(0, p):
190
+ processes.append(ctx.Process(target=gene_profile_worker, args=(gene_cmd_queue, gene_result_queue)))
191
+ for proc in processes:
192
+ proc.start()
193
+
194
+ # Set up progress bar
195
+ pbar = tqdm(desc='Profiling genes: ', total=len(cmd_groups))
196
+
197
+ # Get the results
198
+ recieved_profiles = 0
199
+ while recieved_profiles < len(cmd_groups):
200
+ GPs = gene_result_queue.get()
201
+ recieved_profiles += 1
202
+ pbar.update(1)
203
+ for GP in GPs:
204
+ if GP is not None:
205
+ logging.debug(GP[4])
206
+ GeneProfiles.append(GP)
207
+
208
+ # Close multi-processing
209
+ for proc in processes:
210
+ proc.terminate()
211
+
212
+ # Close progress bar
213
+ pbar.close()
214
+
215
+ else:
216
+ gene_profile_worker(gene_cmd_queue, gene_result_queue, single_thread=True)
217
+ logging.info("Done profiling genes")
218
+
219
+ # Get the genes
220
+ recieved_profiles = 0
221
+ while recieved_profiles < len(cmd_groups):
222
+ logging.debug('going to grab at {0}'.format(recieved_profiles))
223
+ GPs = gene_result_queue.get(timeout=5)
224
+ logging.debug('did a grab at {0}'.format(recieved_profiles))
225
+ recieved_profiles += 1
226
+ for GP in GPs:
227
+ if GP is not None:
228
+ logging.debug(GP[4])
229
+ GeneProfiles.append(GP)
230
+
231
+ inStrain.logUtils.log_checkpoint("GeneProfile", "return_results", "start")
232
+ name2result = {}
233
+ for i, name in enumerate(['coverage', 'clonality', 'SNP_density', 'SNP_mutation_types']):
234
+ name2result[name] = pd.concat([G[i] for G in GeneProfiles])
235
+ inStrain.logUtils.log_checkpoint("GeneProfile", "return_results", "end")
236
+
237
+ inStrain.logUtils.log_checkpoint("GeneProfile", "calculate_gene_metrics", "end")
238
+ return name2result
239
+
240
+ def profile_genes(scaffold, **kwargs):
241
+ '''
242
+ This is the money that gets multiprocessed
243
+
244
+ Relies on having a global "Gdb", "gene2sequence", "CumulativeSNVtable", "covTs", and "clonTs"
245
+
246
+ * Calculate the clonality, coverage, linkage, and SNV_density for each gene
247
+ * Determine whether each SNP is synynomous or nonsynonymous
248
+ '''
249
+ # Log
250
+ pid = os.getpid()
251
+ log_message = "\nSpecialPoint_genes {0} PID {1} whole start {2}".format(scaffold, pid, time.time())
252
+
253
+ # For testing purposes
254
+ if ((scaffold == 'FailureScaffoldHeaderTesting')):
255
+ assert False
256
+
257
+ # Get the list of genes for this scaffold
258
+ gdb = Gdb[Gdb['scaffold'] == scaffold]
259
+
260
+ # Calculate gene-level coverage
261
+ log_message += "\nSpecialPoint_genes {0} PID {1} coverage start {2}".format(scaffold, pid, time.time())
262
+ if scaffold not in covTs:
263
+ logging.info("{0} isnt in covT!".format(scaffold))
264
+ cdb = pd.DataFrame()
265
+ else:
266
+ covT = covTs[scaffold]
267
+ cdb = calc_gene_coverage(gdb, covT)
268
+ del covT
269
+ log_message += "\nSpecialPoint_genes {0} PID {1} coverage end {2}".format(scaffold, pid, time.time())
270
+
271
+ # Calculate gene-level clonality
272
+ log_message += "\nSpecialPoint_genes {0} PID {1} clonality start {2}".format(scaffold, pid, time.time())
273
+ if scaffold not in clonTs:
274
+ logging.info("{0} isnt in clovT!".format(scaffold))
275
+ cldb = pd.DataFrame()
276
+ else:
277
+ clonT = clonTs[scaffold]
278
+ cldb = calc_gene_clonality(gdb, clonT)
279
+ del clonT
280
+ log_message += "\nSpecialPoint_genes {0} PID {1} clonality end {2}".format(scaffold, pid, time.time())
281
+
282
+ # Determine whether SNPs are synonmous or non-synonmous
283
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_character start {2}".format(scaffold, pid, time.time())
284
+ Ldb = CumulativeSNVtable[CumulativeSNVtable['scaffold'] == scaffold]
285
+ if len(Ldb) == 0:
286
+ sdb = pd.DataFrame()
287
+ else:
288
+ sdb = Characterize_SNPs_wrapper(Ldb, gdb, scaff2gene2sequence[scaffold])
289
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_character end {2}".format(scaffold, pid, time.time())
290
+
291
+ # Calculate gene-level SNP counts
292
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts start {2}".format(scaffold, pid, time.time())
293
+ if len(Ldb) == 0:
294
+ ldb = pd.DataFrame()
295
+ sublog = ''
296
+ else:
297
+ #ldb = calc_gene_snp_density(gdb, Ldb)
298
+ ldb, sublog = calc_gene_snp_counts(gdb, Ldb, sdb, scaff2gene2sequence[scaffold], scaffold=scaffold)
299
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts end {2}".format(scaffold, pid, time.time())
300
+ log_message += sublog
301
+
302
+ log_message += "\nSpecialPoint_genes {0} PID {1} whole end {2}".format(scaffold, pid, time.time())
303
+
304
+ results = (cdb, cldb, ldb, sdb, log_message)
305
+
306
+ return results
307
+
308
+ def profile_genes_from_profile(scaffold, gdb, covT, clonT, Ldb, gene2sequence):
309
+ '''
310
+ Call profile genes from elsewhere
311
+
312
+ Arguments:
313
+ scaffold = name of scaffold
314
+ gdb = gene_datatable
315
+ covT = covT for this scaffold
316
+ clonT = clonT for this scaffold
317
+ Ldb = cumulative SNP table for this scaffold
318
+
319
+ * Calculate the clonality, coverage, linkage, and SNV_density for each gene
320
+ * Determine whether each SNP is synynomous or nonsynonymous
321
+ '''
322
+ # Log
323
+ log = inStrain.logUtils.get_worker_log('ProfileGenes', scaffold, 'start')
324
+
325
+ # For testing purposes
326
+ if ((scaffold == 'FailureScaffoldHeaderTesting')):
327
+ assert False
328
+
329
+ # Calculate gene-level coverage
330
+ #log_message += "\nSpecialPoint_genes {0} PID {1} coverage start {2}".format(scaffold, pid, time.time())
331
+ cdb = calc_gene_coverage(gdb, covT)
332
+ #log_message += "\nSpecialPoint_genes {0} PID {1} coverage end {2}".format(scaffold, pid, time.time())
333
+
334
+ # Calculate gene-level clonality
335
+ # log_message += "\nSpecialPoint_genes {0} PID {1} clonality start {2}".format(scaffold, pid, time.time())
336
+ cldb = calc_gene_clonality(gdb, clonT)
337
+ # log_message += "\nSpecialPoint_genes {0} PID {1} clonality end {2}".format(scaffold, pid, time.time())
338
+
339
+ # Determine whether SNPs are synonmous or non-synonmous
340
+ #log_message += "\nSpecialPoint_genes {0} PID {1} SNP_character start {2}".format(scaffold, pid, time.time())
341
+ sdb = Characterize_SNPs_wrapper(Ldb, gdb, gene2sequence)
342
+ #log_message += "\nSpecialPoint_genes {0} PID {1} SNP_character end {2}".format(scaffold, pid, time.time())
343
+
344
+ # Calculate gene-level SNP counts
345
+ #log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts start {2}".format(scaffold, pid, time.time())
346
+ ldb, sublog = calc_gene_snp_counts(gdb, Ldb, sdb, gene2sequence, scaffold=scaffold)
347
+ #log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts end {2}".format(scaffold, pid, time.time())
348
+ #log_message += sublog
349
+
350
+ log += inStrain.logUtils.get_worker_log('ProfileGenes', scaffold, 'end')
351
+
352
+ results = (cdb, cldb, ldb, sdb, log)
353
+
354
+ return results
355
+
356
+ def calc_gene_coverage(gdb, covT):
357
+ '''
358
+ Gene-level and mm-level coverage
359
+ '''
360
+ table = defaultdict(list)
361
+
362
+ for mm, cov in iterate_covT_mms(covT):
363
+ if len(cov) == 0:
364
+ continue
365
+
366
+ for i, row in gdb.iterrows():
367
+ gcov = cov.loc[int(row['start']):int(row['end'])]
368
+ gLen = abs(row['end'] - row['start']) + 1
369
+
370
+ table['gene'].append(row['gene'])
371
+ table['coverage'].append(gcov.sum() / gLen)
372
+ table['breadth'].append(len(gcov) / gLen)
373
+ table['mm'].append(mm)
374
+
375
+ return pd.DataFrame(table)
376
+
377
+ def iterate_clonT_mms(clonT):
378
+ p2c = {}
379
+ mms = sorted([int(mm) for mm in list(clonT.keys())])
380
+ for mm in mms:
381
+ for pos, val in clonT[mm].items():
382
+ p2c[pos] = val
383
+
384
+ inds = []
385
+ vals = []
386
+ for ind in sorted(p2c.keys()):
387
+ inds.append(ind)
388
+ vals.append(p2c[ind])
389
+
390
+ yield mm, pd.Series(data = vals, index = np.array(inds).astype('int'), dtype='float64')
391
+
392
+ def iterate_covT_mms(clonT):
393
+ counts = pd.Series(dtype='float64')
394
+ mms = sorted([int(mm) for mm in list(clonT.keys())])
395
+ for mm in mms:
396
+ count = clonT[mm]
397
+ counts = counts.add(count, fill_value=0)
398
+ yield mm, counts
399
+
400
+ def calc_gene_clonality(gdb, clonT):
401
+ '''
402
+ Gene-level and mm-level clonality
403
+ '''
404
+ table = defaultdict(list)
405
+
406
+ for mm, cov in iterate_clonT_mms(clonT):
407
+ if len(cov) == 0:
408
+ continue
409
+
410
+ for i, row in gdb.iterrows():
411
+ gcov = cov.loc[int(row['start']):int(row['end'])]
412
+ gLen = abs(row['end'] - row['start']) + 1
413
+
414
+ table['gene'].append(row['gene'])
415
+
416
+ try:
417
+ microdiversity = 1 - gcov.mean()
418
+ except :
419
+ microdiversity = np.nan
420
+
421
+ #table['clonality'].append(gcov.mean())
422
+ table['nucl_diversity'].append(microdiversity)
423
+ table['breadth_minCov'].append(len(gcov) / gLen)
424
+ table['mm'].append(mm)
425
+
426
+ return pd.DataFrame(table)
427
+ #
428
+ # def calc_gene_snp_density(gdb, ldb):
429
+ # '''
430
+ # Gene-level and mm-level clonality
431
+ # '''
432
+ # table = defaultdict(list)
433
+ #
434
+ # for mm in sorted(ldb['mm'].unique()):
435
+ # db = ldb[ldb['mm'] <= mm].drop_duplicates(subset=['scaffold', 'position'], keep='last')
436
+ # cov = db.set_index('position')['ref_base'].sort_index()
437
+ # if len(cov) == 0:
438
+ # continue
439
+ #
440
+ # for i, row in gdb.iterrows():
441
+ # gcov = cov.loc[int(row['start']):int(row['end'])]
442
+ # gLen = abs(row['end'] - row['start']) + 1
443
+ #
444
+ # table['gene'].append(row['gene'])
445
+ # table['SNPs_per_bp'].append(len(gcov) / gLen)
446
+ # table['mm'].append(mm)
447
+ #
448
+ # return pd.DataFrame(table)
449
+
450
+
451
+
452
+ def count_sites(seq, k=1, codon_table=None):
453
+ '''
454
+ From a nucleotide sequence and codon table, calculate S and N sites
455
+ '''
456
+ codon_lst = convert_to_codons(seq)
457
+
458
+ if codon_table is None:
459
+ codon_table = default_codon_table
460
+ S_site = 0.0 # synonymous sites
461
+ N_site = 0.0 # non-synonymous sites
462
+ purine = ('A', 'G')
463
+ pyrimidine = ('T', 'C')
464
+ base_tuple = ('A', 'T', 'C', 'G')
465
+ for codon in codon_lst:
466
+ neighbor_codon = {'transition': [], 'transversion': []}
467
+ # classify neighbor codons
468
+ codon = codon.replace('U', 'T')
469
+ if codon == '---':
470
+ continue
471
+ if 'N' in codon:
472
+ continue
473
+ for n, i in enumerate(codon):
474
+ for j in base_tuple:
475
+ if i == j:
476
+ pass
477
+ elif i in purine and j in purine:
478
+ codon_chars = [c for c in codon]
479
+ codon_chars[n] = j
480
+ this_codon = ''.join(codon_chars)
481
+ neighbor_codon['transition'].append(this_codon)
482
+ elif i in pyrimidine and j in pyrimidine:
483
+ codon_chars = [c for c in codon]
484
+ codon_chars[n] = j
485
+ this_codon = ''.join(codon_chars)
486
+ neighbor_codon['transition'].append(this_codon)
487
+ else:
488
+ codon_chars = [c for c in codon]
489
+ codon_chars[n] = j
490
+ this_codon = ''.join(codon_chars)
491
+ neighbor_codon['transversion'].append(this_codon)
492
+ # count synonymous and non-synonymous sites
493
+ #codon = codon.replace('T', 'U')
494
+ if (codon in ['TAG', 'TAA', 'TGA']):
495
+ #print("STOP DETECTED")
496
+ continue
497
+ aa = codon_table.forward_table[codon]
498
+ this_codon_N_site = this_codon_S_site = 0
499
+ for neighbor in neighbor_codon['transition']:
500
+ if neighbor in codon_table.stop_codons:
501
+ this_codon_N_site += 1
502
+ elif codon_table.forward_table[neighbor] == aa:
503
+ this_codon_S_site += 1
504
+ else:
505
+ this_codon_N_site += 1
506
+ for neighbor in neighbor_codon['transversion']:
507
+ if neighbor in codon_table.stop_codons:
508
+ this_codon_N_site += k
509
+ elif codon_table.forward_table[neighbor] == aa:
510
+ this_codon_S_site += k
511
+ else:
512
+ this_codon_N_site += k
513
+ norm_const = (this_codon_N_site + this_codon_S_site)/3
514
+ S_site += float(this_codon_S_site) / float(norm_const)
515
+ N_site += float(this_codon_N_site) / float(norm_const)
516
+ return (S_site, N_site)
517
+
518
+ def convert_to_codons(seq):
519
+ codons = []
520
+ for c in zip(*(iter(seq),) * 3):
521
+ co = ''.join(c)
522
+ assert len(co) == 3
523
+ codons.append(co)
524
+ return codons
525
+
526
+ def calc_gene_snp_counts(gdb, ldb, sdb, gene2sequence, scaffold=None):
527
+ '''
528
+ Count the number of SNPs in each gene, as well as N and S sites
529
+
530
+ RELIES ON HAVING gene2sequence AS A GLOBAL (needed for multiprocessing speed)
531
+
532
+ Argumnets:
533
+ gdb = table of genes
534
+ ldb = Raw cumulative snp table for a single scaffold (mm-level)
535
+ sdb = SNP table with N and S and I annotated
536
+ '''
537
+ if len(ldb) == 0:
538
+ return pd.DataFrame(), ''
539
+
540
+ pid = os.getpid()
541
+
542
+ # Merge ldb and sdb
543
+ xdb = pd.merge(ldb, sdb[['position', 'mutation_type', 'gene']],
544
+ on=['position'], how='left').reset_index(drop=True)
545
+
546
+ # Calculate counts of N and S sites
547
+ log_message = "\nSpecialPoint_genes {0} PID {1} SNP_counts_SiteCalc start {2}".format(scaffold, pid, time.time())
548
+ table = defaultdict(list)
549
+ for i, row in gdb.iterrows():
550
+ try:
551
+ S_site, N_site = count_sites(gene2sequence[row['gene']])
552
+ except:
553
+ S_site = np.nan
554
+ N_site = np.nan
555
+
556
+ table['gene'].append(row['gene'])
557
+ table['S_sites'].append(S_site)
558
+ table['N_sites'].append(N_site)
559
+ SiteDb = pd.DataFrame(table)
560
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts_SiteCalc end {2}".format(scaffold, pid, time.time())
561
+
562
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts_geneCalc start {2}".format(scaffold, pid, time.time())
563
+ table = defaultdict(list)
564
+ for mm in sorted(xdb['mm'].unique()):
565
+ # Filter to this mm level and set up for quick indexing
566
+ fdb = xdb[xdb['mm'] <= mm].sort_values('mm').drop_duplicates(subset=['scaffold', 'position'], keep='last').sort_values('position').set_index('position')
567
+
568
+ for i, row in gdb.iterrows():
569
+ # Calculate gene length
570
+ gLen = abs(row['end'] - row['start']) + 1
571
+
572
+ # Subset to this gene
573
+ db = fdb.loc[int(row['start']):int(row['end'])]
574
+
575
+ # Report summary stuff
576
+ table['mm'].append(mm)
577
+ table['gene'].append(row['gene'])
578
+ table['gene_length'].append(gLen)
579
+ table['divergent_site_count'].append(len(db))
580
+
581
+ # Report type counts (SNS = 1 allele; SNV = more than 1 allele, same as at the scaffold level)
582
+ for name, is_type in zip(['SNS', 'SNV'], [db['allele_count'] == 1, db['allele_count'] > 1]):
583
+ table['{0}_count'.format(name)].append(len(db[is_type]))
584
+
585
+ for snp_type in ['N', 'S']:
586
+ table["{0}_{1}_count".format(name, snp_type)].append(
587
+ len(db[is_type & (db['mutation_type'] == snp_type)]))
588
+
589
+ GGdb = pd.DataFrame(table).merge(SiteDb, on='gene', how='left').reset_index(drop=True)
590
+ log_message += "\nSpecialPoint_genes {0} PID {1} SNP_counts_geneCalc end {2}".format(scaffold, pid, time.time())
591
+
592
+ # Calculate dn/ds
593
+ GGdb['dNdS_substitutions'] = [((nC/nS) / (sC/sS)) if ((sC > 0) & (sS > 0)) else np.nan for nC, nS, sC, sS in zip(
594
+ GGdb['SNS_N_count'], GGdb['N_sites'],
595
+ GGdb['SNS_S_count'], GGdb['S_sites'])]
596
+ GGdb['pNpS_variants'] = [((nC/nS) / (sC/sS)) if ((sC > 0) & (sS > 0)) else np.nan for nC, nS, sC, sS in zip(
597
+ GGdb['SNV_N_count'], GGdb['N_sites'],
598
+ GGdb['SNV_S_count'], GGdb['S_sites'])]
599
+ # GGdb['SNPs_per_bp'] = [x/y if y > 0 else np.nan for x, y in \
600
+ # zip(GGdb['divergent_site_count'], GGdb['gene_length'])]
601
+
602
+ return GGdb, log_message
603
+
604
+ def Characterize_SNPs_wrapper(Ldb, gdb, gene2sequence):
605
+ '''
606
+ A wrapper for characterizing SNPs
607
+
608
+ RELIES ON HAVING gene2sequence AS A GLOBAL (needed for multiprocessing speed)
609
+
610
+ Arguments:
611
+ Ldb = CumulativeSNVtable for a single scaffold
612
+ gdb = table of genes
613
+
614
+ Returns:
615
+ Sdb = The Cumulative SNV table with extra information added
616
+ '''
617
+ if len(Ldb) == 0:
618
+ return pd.DataFrame()
619
+
620
+ # Get a non-nonredundant list of SNPs (the highest mm, to match the SNVs.tsv output)
621
+ Sdb = Ldb.sort_values('mm').drop_duplicates(subset=['scaffold', 'position'], keep='last')\
622
+ .sort_index().drop(columns=['mm'])
623
+ Sdb['position'] = Sdb['position'].astype(int)
624
+
625
+ # Filter out SNPs that shouldn't be profiled like this
626
+ # Sdb = Sdb[Sdb['cryptic'] == False]
627
+ # Sdb = Sdb.drop(columns="cryptic")
628
+
629
+ if 'morphia' in Sdb.columns:
630
+ col = 'morphia'
631
+ else:
632
+ col = 'allele_count'
633
+ Sdb[col] = Sdb[col].astype(int)
634
+ # Sites with more than 2 alleles are characterized using the consensus and variant (2nd most common) base
635
+ Sdb = Sdb[Sdb[col] > 0]
636
+
637
+ # Make sure some SNPs to profile remain
638
+ if len(Sdb) == 0:
639
+ return pd.DataFrame()
640
+
641
+ # Characterize
642
+ sdb = characterize_SNPs(gdb, Sdb, gene2sequence)
643
+ assert len(Sdb) == len(sdb)
644
+ sdb = pd.merge(Sdb, sdb, on=['position'], how='left').reset_index(drop=True)
645
+
646
+ # Return
647
+ return sdb
648
+
649
+ def characterize_SNPs(gdb, Sdb, gene2sequence):
650
+ '''
651
+ Determine the type of SNP (synonymous, non-synynomous, or intergenic)
652
+
653
+ RELIES ON HAVING gene2sequence AS A GLOBAL (needed for multiprocessing speed)
654
+ '''
655
+ table = defaultdict(list)
656
+ for i, row in Sdb.iterrows():
657
+ db = gdb[(gdb['start'] <= row['position']) & (gdb['end'] >= row['position'])]
658
+ if len(db) == 0:
659
+ table['position'].append(row['position'])
660
+ table['mutation_type'].append('I')
661
+ table['mutation'].append('')
662
+ table['gene'].append('')
663
+ elif len(db) > 1:
664
+ table['position'].append(row['position'])
665
+ table['mutation_type'].append('M')
666
+ table['mutation'].append('')
667
+ table['gene'].append(','.join(db['gene'].tolist()))
668
+ else:
669
+ # Get the original sequence
670
+ original_sequence = gene2sequence[db ['gene'].tolist()[0]]
671
+ if db['direction'].tolist()[0] == '-1':
672
+ original_sequence = original_sequence.reverse_complement()
673
+
674
+ # Make the new sequence
675
+ snp_start = row['position'] - db['start'].tolist()[0]
676
+ new_sequence = MutableSeq(str(original_sequence))
677
+ try:
678
+ new_sequence[snp_start] = row['con_base']
679
+ except:
680
+ print(f'ERROR!! {row} {db["gene"].tolist()[0]} {len(new_sequence)}\n{new_sequence}\n{db}')
681
+
682
+ if new_sequence[snp_start] == original_sequence[snp_start]:
683
+ new_sequence[snp_start] = row['var_base']
684
+ new_sequence = Seq(str(new_sequence))
685
+
686
+ # Translate
687
+ if db['direction'].tolist()[0] == '-1':
688
+ old_aa_sequence = original_sequence.reverse_complement().translate()
689
+ new_aa_sequence = new_sequence.reverse_complement().translate()
690
+ else:
691
+ old_aa_sequence = original_sequence.translate()
692
+ new_aa_sequence = new_sequence.translate()
693
+
694
+ # old_aa_sequence = original_sequence.translate()
695
+ # new_aa_sequence = new_sequence.translate()
696
+
697
+ # Find mutation
698
+ mut_type = 'S'
699
+ mut = 'S:' + str(snp_start)
700
+ for aa in range(0, len(old_aa_sequence)):
701
+ if new_aa_sequence[aa] != old_aa_sequence[aa]:
702
+ mut_type = 'N'
703
+ mut = 'N:' + str(old_aa_sequence[aa]) + str(snp_start) + str(new_aa_sequence[aa])
704
+ break
705
+
706
+ # Store
707
+ table['position'].append(row['position'])
708
+ table['mutation_type'].append(mut_type)
709
+ table['mutation'].append(mut)
710
+ table['gene'].append(db['gene'].tolist()[0])
711
+
712
+ return pd.DataFrame(table)
713
+
714
+ def iterate_commands(scaffolds_to_profile, Gdb, kwargs):
715
+ '''
716
+ Break into individual scaffolds
717
+ '''
718
+ processes = kwargs.get('processes', 6)
719
+ s2g = kwargs.get('s2g', None)
720
+ SECONDS = min(60, sum(calc_estimated_runtime(s2g[scaffold]) for scaffold in scaffolds_to_profile)/(processes+1))
721
+
722
+ cmds = []
723
+ seconds = 0
724
+ for scaffold, gdb in Gdb.groupby('scaffold'):
725
+ if scaffold not in scaffolds_to_profile:
726
+ continue
727
+
728
+ # make this comammand
729
+ cmd = Command()
730
+ cmd.scaffold = scaffold
731
+ cmd.arguments = kwargs
732
+
733
+ # Add estimated seconds
734
+ seconds += calc_estimated_runtime(s2g[scaffold])
735
+ cmds.append(cmd)
736
+
737
+ # See if you're done
738
+ if seconds >= SECONDS:
739
+ yield cmds
740
+ seconds = 0
741
+ cmds = []
742
+
743
+ yield cmds
744
+
745
+ def calc_estimated_runtime(pairs):
746
+ SLOPE_CONSTANT = 0.01
747
+ return pairs * SLOPE_CONSTANT
748
+
749
+
750
+
751
+ class Command():
752
+ def __init__(self):
753
+ pass
754
+
755
+ def parse_genes(gene_file_loc, **kwargs):
756
+ '''
757
+ Parse a file of genes based on the file extention.
758
+
759
+ Currently supported extentions are:
760
+ .fna (prodigal)
761
+ .gb / .gbk (genbank)
762
+
763
+ Methods return a table of genes (Gdb) and a dictionary of gene -> sequence
764
+ '''
765
+ if ((gene_file_loc[-4:] == '.fna') | (gene_file_loc[-3:] == '.fa')):
766
+ return parse_prodigal_genes(gene_file_loc)
767
+
768
+ elif ((gene_file_loc[-3:] == '.gb') | (gene_file_loc[-4:] == '.gbk')):
769
+ return parse_genbank_genes(gene_file_loc)
770
+
771
+ else:
772
+ print("I dont know how to process {0}".format(gene_file_loc))
773
+ raise Exception
774
+
775
+ def parse_prodigal_genes(gene_fasta):
776
+ '''
777
+ Parse the prodigal .fna file
778
+
779
+ Return a datatable with gene info and a dictionary of gene -> sequence
780
+ '''
781
+ scaff2gene2sequence = {}
782
+ scaff2geneinfo = {}
783
+ for record in SeqIO.parse(gene_fasta, 'fasta'):
784
+ gene = str(record.id)
785
+ scaff = "_".join(gene.split("_")[:-1])
786
+
787
+ if scaff not in scaff2geneinfo:
788
+ scaff2geneinfo[scaff] = defaultdict(list)
789
+ table = scaff2geneinfo[scaff]
790
+
791
+ table['gene'].append(gene)
792
+ table['scaffold'].append(scaff)
793
+ table['direction'].append(record.description.split("#")[3].strip())
794
+ table['partial'].append('partial=01' in record.description)
795
+
796
+ # NOTE: PRODIGAL USES A 1-BASED INDEX AND WE USE 0, SO CONVERT TO 0 HERE
797
+ start = int(record.description.split("#")[1].strip())-1
798
+ end = int(record.description.split("#")[2].strip())-1
799
+ table['start'].append(start)
800
+ table['end'].append(end)
801
+
802
+ if scaff not in scaff2gene2sequence:
803
+ scaff2gene2sequence[scaff] = {}
804
+ scaff2gene2sequence[scaff][gene] = record.seq
805
+
806
+ # Ensure the start and ends are parsed correctly
807
+ if (end - start) + 1 != len(record.seq):
808
+ logging.error(f"ERROR PARSING GENE {gene}; start={start} end={end} length={len(record.seq)}")
809
+ logging.error(f"Make sure that you report genes with 1-based indexing and with an INCLUSIVE end")
810
+
811
+
812
+ scaffs = list(scaff2geneinfo.keys())
813
+ for scaff in scaffs:
814
+ scaff2geneinfo[scaff] = pd.DataFrame(scaff2geneinfo[scaff])
815
+
816
+ return scaff2geneinfo, scaff2gene2sequence
817
+
818
+ def parse_genbank_genes(gene_file, gene_name='gene'):
819
+ '''
820
+ Parse a genbank file. Gets features marked as CDS
821
+ '''
822
+ scaff2geneinfo = {}
823
+ scaff2gene2sequence = {}
824
+ for record in SeqIO.parse(gene_file, 'gb'):
825
+ scaffold = record.id
826
+ for feature in record.features:
827
+ if feature.type == 'CDS':
828
+ gene = feature.qualifiers[gene_name][0]
829
+ loc = feature.location
830
+ if type(loc) is Bio.SeqFeature.CompoundLocation:
831
+ partial = 'compound'
832
+ else:
833
+ partial = False
834
+
835
+ if scaffold not in scaff2geneinfo:
836
+ scaff2geneinfo[scaffold] = defaultdict(list)
837
+ table = scaff2geneinfo[scaffold]
838
+
839
+ table['gene'].append(gene)
840
+ table['scaffold'].append(scaffold)
841
+ table['direction'].append(feature.location.strand)
842
+ table['partial'].append(partial)
843
+
844
+ table['start'].append(loc.start)
845
+ table['end'].append(loc.end - 1)
846
+
847
+ if scaffold not in scaff2gene2sequence:
848
+ scaff2gene2sequence[scaffold] = {}
849
+ scaff2gene2sequence[scaffold][gene] = feature.location.extract(record).seq
850
+
851
+ scaffs = list(scaff2geneinfo.keys())
852
+ for scaff in scaffs:
853
+ scaff2geneinfo[scaff] = pd.DataFrame(scaff2geneinfo[scaff])
854
+
855
+ return scaff2geneinfo, scaff2gene2sequence
856
+
857
+ def get_gene_info(IS, ANI_level=0):
858
+ #IS = inStrain.SNVprofile.SNVprofile(IS_loc)
859
+
860
+ # Get the mm level
861
+ mm = _get_mm(IS, ANI_level)
862
+
863
+ # Load all genes
864
+ Gdb = IS.get('genes_table')
865
+
866
+ # Load coverage, clonality, and SNPs
867
+ for thing in ['genes_coverage', 'genes_clonality', 'genes_SNP_count']:
868
+ db = IS.get(thing)
869
+ if db is None:
870
+ logging.debug('Skipping {0} gene calculation; you have none'.format(thing))
871
+ continue
872
+ if len(db) == 0:
873
+ logging.debug('Skipping {0} gene calculation; you have none'.format(thing))
874
+ continue
875
+ db = db[db['mm'] <= mm].sort_values('mm').drop_duplicates(subset=['gene'], keep='last')
876
+ del db['mm']
877
+ Gdb = pd.merge(Gdb, db, on='gene', how='left')
878
+
879
+ Gdb['min_ANI'] = ANI_level
880
+
881
+ return Gdb
882
+
883
+ def _get_mm(IS, ANI):
884
+ '''
885
+ Get the mm corresponding to an ANI level in an IS
886
+ '''
887
+ if ANI > 1:
888
+ ANI = ANI / 100
889
+
890
+ rLen = IS.get('mapping_info')['mean_pair_length'].tolist()[0]
891
+ mm = int(round((rLen - (rLen * ANI))))
892
+ return mm