-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_sparta.py
More file actions
368 lines (292 loc) · 16.5 KB
/
Copy pathtest_sparta.py
File metadata and controls
368 lines (292 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#! /usr/bin/python
# -*- coding: utf-8 -*-
"""
Unit tests for the compare_mappings.py module
Created on Tue Jun 17 16:44:51 2014
@author: Peter Edge
"""
from __future__ import print_function
import unittest
import filecmp
import math
import os
import shutil
import sparta
import pysam
# take a sequence, quality string, and MD string and return an aligned_read object
def read_gen(seq, qual, MD, cigar=None):
read = pysam.AlignedRead()
read.seq = seq
read.qual = qual
read.tags = [('MD',MD)]
read.cigar = cigar
return read
# tests for the matched base probability calculator
class test_matched_prob(unittest.TestCase):
# values were hand-computed in wolframalpha: formula log10(1-(10^((x-33)/-10)))
# where x was obtained from 'dec' column in the ascii table at http://www.asciitable.com/
def setUp(self):
self.separator = sparta.multimapped_read_separator(genome_priors=[0.5, 0.5])
def test_F(self):
prob = self.separator.log10_matched_base_prob[70]
self.assertAlmostEqual(prob, -0.0000866617872714981203221)
def test_crunch(self):
prob = self.separator.log10_matched_base_prob[35]
self.assertAlmostEqual(prob, -0.4329234333362482973965396)
def test_tilde(self):
prob = self.separator.log10_matched_base_prob[126]
self.assertAlmostEqual(prob, -2.1766285001922517006651895e-10)
# tests for the mismatched base probability calculator
class test_mismatched_prob(unittest.TestCase):
# values were hand-computed in wolframalpha: formula log10((10^((x-33)/-10))/3)
# where x was obtained from the 'dec' column ascii table at http://www.asciitable.com/
def setUp(self):
self.separator = sparta.multimapped_read_separator(genome_priors=[0.5, 0.5])
def test_F(self):
prob = self.separator.log10_mismatched_base_prob[('A','T',70)]
self.assertAlmostEqual(prob, -4.1771212547196624372950279)
def test_crunch(self):
prob = self.separator.log10_mismatched_base_prob[('G','T',35)]
self.assertAlmostEqual(prob, -0.6771212547196624372950279)
def test_tilde(self):
prob = self.separator.log10_mismatched_base_prob[('C', 'A', 126)]
self.assertAlmostEqual(prob, -9.7771212547196624372950279)
# tests for the method that acumulates probabilities for a whole aligned read
class test_aligned_read_prob(unittest.TestCase):
def setUp(self):
self.separator = sparta.multimapped_read_separator(genome_priors=[0.5, 0.5])
# test values were hand-computed using wolfram alpha
# this read is expected to yield a high probability
def test_likely_read(self):
read = read_gen('ATGCAAAGGC','FAAAFFF5FF','10')
prob = self.separator.aligned_read_prob(read)
self.assertAlmostEqual(prob, 0.98694488490516351120939182)
# this read has matches with probability 0 so result should be 0
def test_unlikely_read_bc_garbage_quality(self):
read = read_gen('ATGCAAAGGC','!2222!!111','10')
prob = self.separator.aligned_read_prob(read)
self.assertAlmostEqual(prob, 0.0)
# this read is high quality but has an error; should have low probability
def test_unlikely_read_bc_high_qual_mismatch(self):
read = read_gen('ATGCAAAGGC','JJJJJJJJJJ','2A7')
prob = self.separator.aligned_read_prob(read)
self.assertAlmostEqual(prob, 0.00002645868511694054719287)
# this read has two high-quality mismatches and therefore should have very
# low probability.
def test_unlikely_read_bc_2_high_qual_mismatch(self):
read = read_gen('ATGCAAAGGC','JJJJJJJJJJ','0G4G4')
prob = self.separator.aligned_read_prob(read)
self.assertAlmostEqual(math.log10(prob), math.log10(7.0061834016176909988601998e-10))
# slightly longer read, many more mismatches
def test_longer_unlikely_read(self):
read = (read_gen('TTTTTTTTTTATGCAAAGGCATGCAAAGGCATGCAAAGGC',
'JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ',
'0A0A0A0A0A0A0A0A0A0A30'))
prob = self.separator.aligned_read_prob(read)
self.assertAlmostEqual(math.log10(prob), math.log10(1.6894778199911023692428023e-46))
# this read is 60 bp in length
# half of the read are high-quality mismatches (30 total)
# this would never happen in real life
def test_longest_read_many_mismatch(self):
read = (read_gen('TTTTTTTTTTTTTTTTTTTTATGCAAAGGCTTTTTTTTTTATGCAAAGGCATGCAAAGGC',
'JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ',
'0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A10A0A0A0A0A0A0A0A0A0A20'))
prob = self.separator.aligned_read_prob(read)
# computed on wolframalpha like so:
# (10^(((74-33)*-0.1)-log10(3)))^30 * (1-10^(((74-33)*-0.1)))^30
self.assertAlmostEqual(math.log10(prob), math.log10(4.84537506679955566671356787e-138))
# same test as before but do it 1e5 times to test overhead of prob lookup
'''
def test_lookup_overhead(self):
for i in range(0,10000):
read = (read_gen('TTTTTTTTTTTTTTTTTTTTATGCAAAGGCTTTTTTTTTTATGCAAAGGCATGCAAAGGC',
'JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ',
'0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A10A0A0A0A0A0A0A0A0A0A20'))
prob = self.separator.aligned_read_prob(read)
# computed on wolframalpha like so:
# (10^(((74-33)*-0.1)-log10(3)))^30 * (1-10^(((74-33)*-0.1)))^30
self.assertAlmostEqual(math.log10(prob), math.log10(4.84537506679955566671356787e-138))
'''
# tests for the untangle_mappings method, which takes reads that are mapped
# to two different genomes and tries to assign them to the correct one.
class test_untangle(unittest.TestCase):
def setUp(self):
self.separator = sparta.multimapped_read_separator(genome_priors=[0.5, 0.5])
# variable to hold prob_genome1 in return result.
NIL = 0.0
NIL2 = 0
# if neither sequence has errors then it is impossible to map
# result should be unmapped
def test_untangle_both_errorfree(self):
read1 = read_gen('AAAAAAAAAA','FFFFFFFFFF','10')
read2 = read_gen('AAAAAAAAAA','FFFFFFFFFF','10')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'unclassified')
# if read1 has one error then the result should be classified2
def test_untangle_one_error(self):
read1 = read_gen('AAATAAAAAA','FFFFFFFFFF','3A6')
read2 = read_gen('AAAAAAAAAA','FFFFFFFFFF','10')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified2')
# if read1 is all errors then the result should be classified2
def test_untangle_all_errors(self):
read1 = read_gen('TTTTTTTTTT','FFFFFFFFFF','0A0A0A0A0A0A0A0A0A0A0')
read2 = read_gen('AAAAAAAAAA','FFFFFFFFFF','10')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified2')
# if read1 has 2 errors but read2 has 4, result should be classified1
def test_untangle_one_has_more_errors(self):
read1 = read_gen('AGCAAAAAAA','FFFFFFFFFF','1A0A7')
read2 = read_gen('AAGGAACTAA','FFFFFFFFFF','2A0A2A0A2')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified1')
# if both reads have the same number of errors but genome1's
# errors have low quality,the result should be classified1
def test_untangle_based_on_really_low_qual_errors(self):
read1 = read_gen('AAGCAAAAAA','FF!!FFFFFF','2A0A6')
read2 = read_gen('AAAAAACTAA','FF!!FFFFFF','6A0A2')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified1')
# if both reads have the same number of errors but genome1's errors have low quality,
# the result should be genome1
def test_untangle_based_on_low_qual_errors(self):
read1 = read_gen('AAGCAAAAAA','FF55FFFFFF','2A0A6')
read2 = read_gen('AAAAAACTAA','FF55FFFFFF','6A0A2')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified1')
# test posterier cutoff
# if both reads have the same number of errors and genome1's errors only have
# a slight dip in quality then read is unsorted based on default posterior
# probability cutoff of 0.9
def test_posterior_cutoff(self):
read1 = read_gen('AAGCAAAAAA','FFBBFFFFFF','2A0A6')
read2 = read_gen('AAAAAACTAA','FFBBFFFFFF','6A0A2')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'unclassified')
# if both reads have the same number of errors and genome1's errors only have
# a slight dip in quality then the by severely loosening the posterior
# cutoff then the result should be genome1
def test_loosened_posterior_cutoff(self):
self.separator = sparta.multimapped_read_separator(posterior_cutoff=0.7, genome_priors=[0.5, 0.5])
read1 = read_gen('AAGCAAAAAA','FFAAFFFFFF','2A0A6')
read2 = read_gen('AAAAAACTAA','FFAAFFFFFF','6A0A2')
result, NIL, NIL2 = self.separator.untangle_mappings([read1, read2])
self.assertTrue(result == 'classified1')
# these tests exist to make sure that the proper sequence alignment is acheived
# when weird cigar string cases are encountered. The basic assumption is that
# once a sequence with (for instance) insertions is edited to 'remove' the insertions,
# the MD field of the aligned_read object should align perfectly with it.
# In other words, the number of bases that the MD field specify should match up to
# that of the edited sequence.
class test_cigar_handling(unittest.TestCase):
def setUp(self):
self.separator = sparta.multimapped_read_separator(genome_priors=[0.5, 0.5])
# most of the sequences are modified forms of that used repeatedly in test_untangle
# 3 inserted 'T's
# simply ensure that the program didn't crash
def test_insertion(self):
read = read_gen('AAGTTTCAAAAAA','FFA###AFFFFFF','2A0A6',[(0,3),(1,3),(0,7)])
prob = self.separator.aligned_read_prob(read)
assert prob
# 3 deleted 'T's in the same position
def test_deletion(self):
read = read_gen('AAGCAAAAAA','FFAAFFFFFF','2A0^TTT0A6',[(0,3),(2,3),(0,7)])
prob = self.separator.aligned_read_prob(read)
assert prob
# "skipped region" of 3 'T's in the same position
# this is basically a deletion but is more meant to signify an intron
def test_skipped_region(self):
read = read_gen('AAGCAAAAAA','FFAAFFFFFF','2A0A6',[(0,3),(3,3),(0,7)])
prob = self.separator.aligned_read_prob(read)
assert prob
# soft clipping: 3 'T's at the beginning of the seq are not "counted" by MD
def test_soft_clipping(self):
read = read_gen('TTTAAGCAAAAAA','###FFAAFFFFFF','2A0A6',[(4,3),(0,10)])
prob = self.separator.aligned_read_prob(read)
assert prob
# hard clipping: like a soft clipping except bases aren't present in SEQ
# so really not much can go wrong here
def test_hard_clipping(self):
read = read_gen('AAGCAAAAAA','FFAAFFFFFF','2A0A6',[(5,3),(0,10)])
prob = self.separator.aligned_read_prob(read)
assert prob
# same as the insertion test but also with 2 padding bases
# padding bases do not add anything to the total length of SEQ
def test_padding(self):
read = read_gen('AAGTTTCAAAAAA','FFA###AFFFFFF','2A0A6',[(0,3),(1,3),(6,2),(0,7)])
prob = self.separator.aligned_read_prob(read)
assert prob
# test that the same result is obtained when sparta is run in single core
# (no child processes) mode and multiprocessing mode
class test_multiprocessing(unittest.TestCase):
def setUp(self):
if not os.path.exists('unit_test/output'):
os.makedirs('unit_test/output')
# test single reads on a small (~10K lines) single-read file
def test_single_reads(self):
sams = [os.path.join('unit_test','data','single_reads_genome1.sam'), os.path.join('unit_test','data','single_reads_genome2.sam')]
out_dir = os.path.join('unit_test','output')
files_to_check = ['sorted1.sam', 'sorted2.sam'] #'supplementary_output.txt'
# no multiprocessing
sparta.sparta(sams, num_processes=1, output_dir=os.path.join(out_dir, 'no_mp'),
separated_samfiles=[os.path.join(out_dir,'no_mp','sorted1.sam'), os.path.join(out_dir,'no_mp','sorted2.sam')])
# processes = cpu_count
sparta.sparta(sams, output_dir=os.path.join(out_dir,'mp'),
separated_samfiles=[os.path.join(out_dir,'mp','sorted1.sam'), os.path.join(out_dir,'mp','sorted2.sam')])
# processes = 10
sparta.sparta(sams, num_processes=10, output_dir=os.path.join(out_dir,'mp10'),
separated_samfiles=[os.path.join(out_dir,'mp10','sorted1.sam'), os.path.join(out_dir,'mp10','sorted2.sam')])
match, mismatch, errors = filecmp.cmpfiles(os.path.join(out_dir,'no_mp'), os.path.join(out_dir,'mp'),
common=files_to_check)
match2, mismatch2, errors2 = filecmp.cmpfiles(os.path.join(out_dir,'no_mp'), os.path.join(out_dir,'mp10'),
common=files_to_check)
# check that output was created, and no files mismatched, and no errors occured
assert match != []
assert mismatch == []
assert errors == []
assert match2 != []
assert mismatch2 == []
assert errors2 == []
# clean up
shutil.rmtree(out_dir)
os.makedirs(out_dir)
# test on a small (~10K lines) paired-end read file
def test_paired_end_reads(self):
sams = [os.path.join('unit_test','data','paired_end_reads_genome1.sam'), os.path.join('unit_test','data','paired_end_reads_genome2.sam')]
out_dir = os.path.join('unit_test','output')
files_to_check = ['sorted1.sam', 'sorted2.sam'] # , 'supplementary_output.txt'
# no multiprocessing
sparta.sparta(sams, paired_end=True, num_processes=1, output_dir=os.path.join(out_dir, 'no_mp'),
separated_samfiles=[os.path.join(out_dir,'no_mp','sorted1.sam'),os.path.join(out_dir,'no_mp','sorted2.sam')])
# processes = cpu_count
sparta.sparta(sams, paired_end=True, output_dir=os.path.join(out_dir,'mp'),
separated_samfiles=[os.path.join(out_dir,'mp','sorted1.sam'), os.path.join(out_dir,'mp','sorted2.sam')])
# processes = 10
sparta.sparta(sams, paired_end=True, num_processes=10, output_dir=os.path.join(out_dir,'mp10'),
separated_samfiles=[os.path.join(out_dir,'mp10','sorted1.sam'), os.path.join(out_dir,'mp10','sorted2.sam')])
match, mismatch, errors = filecmp.cmpfiles(os.path.join(out_dir,'no_mp'), os.path.join(out_dir,'mp'),
common=files_to_check)
match2, mismatch2, errors2 = filecmp.cmpfiles(os.path.join(out_dir,'no_mp'), os.path.join(out_dir,'mp10'),
# soft clipping: 3 'T's at the beginning of the seq are not "counted" by MD
common=files_to_check)
if match == []:
import pdb
pdb.set_trace()
# check that output was created, and no files mismatched, and no errors occured
assert match != []
assert mismatch == []
assert errors == []
assert match2 != []
assert mismatch2 == []
assert errors2 == []
# clean up
shutil.rmtree(out_dir)
os.makedirs(out_dir)
if __name__ == '__main__':
# this try/catch block allows test suite to be run in spyder IDE interpreter
# without hanging forever afterwards
#via: http://stackoverflow.com/questions/9202772/tests-succeed-still-get-traceback
try:
unittest.main()
except SystemExit as inst:
pass