4
|
1 #!/usr/bin/env python
|
|
2 """
|
|
3 Snoop thru a fasta file looking for microsatellite repeats of given periods
|
|
4 Output format: length_of_repeat left_flank_length right_flank_length repeat_motif hamming_distance read_name read_sequence read_quality (additional columns)
|
|
5
|
|
6 If --r option turned on, output format will have additional columns behind:
|
|
7 read_name read_chr pre_s pre_e tr_s tr_e suf_s suf_e tr_len tr_ref_seq
|
|
8
|
|
9 pre_s where the read start
|
|
10 pre_e the last position before microsatellite
|
|
11 tr_s where microsatellite start
|
|
12 tr_e where microsatellite end
|
|
13 suf_s first base after microsatellite
|
|
14 tr_ref_seq reference sequence corresponding to microsatellite
|
|
15
|
|
16 * output positions are 0 based
|
|
17
|
|
18 :Author: Chen Sun (cxs1031@cse.psu.edu); Bob Harris (rsharris@bx.psu.edu)
|
|
19
|
|
20 modifing log:
|
|
21
|
|
22 09/27/2013
|
|
23 replace function dense_intervals with function non_negative_intervals, which do not need to import such file.
|
|
24
|
|
25 10/18/2013
|
|
26 modify function find_repeat_element to get a quick speed, under the condition that hamming_distance = 0, which means do not allowed any mutation/indel
|
|
27
|
|
28 02/25/2014
|
|
29 add function that can deal with mapped reads
|
|
30 with additional output
|
|
31
|
|
32 02/28/2014
|
|
33 modify the 0-based end point, as in 0-base area, it is half-open [ )
|
|
34 so the 0-based site, should always be added by 1
|
|
35
|
|
36 03/05/2014
|
|
37 deal with multi-fasta
|
|
38 """
|
|
39 from sys import argv,stdin,stderr,exit
|
|
40 from string import maketrans
|
|
41 from md5 import new as md5_new
|
|
42 import re
|
|
43 #from pyfracluster import dense_intervals
|
|
44
|
|
45 def usage(s=None):
|
|
46 message = """
|
|
47 usage: microsat_snoop [fasta_file] [options]
|
|
48 <fasta_file> Name of file to read sequences from; if absent,
|
|
49 sequences are read from stdin
|
|
50 --fasta Input file is in fasta format
|
|
51 (this is the default)
|
|
52 --fastq Input file is in fastq format
|
|
53 (default is fasta unless filename is .fastq)
|
|
54 --fastq:noquals Input file is in fastq format, but discard quals
|
|
55 --sam Input file is SAM file
|
|
56 --r Indicate additional output information, if indicated,
|
|
57 --ref option is mendatory
|
|
58 --ref=<filepath> Reference file (absolute) path
|
|
59 --period=<length> (mandatory,cumulative) repeat length(s) to be
|
|
60 searched for
|
|
61 <length> is expected to be small, less than 10
|
|
62 <length> can also be a comma-separated list, or
|
|
63 a range <low>..<high>
|
|
64 --rate=<fraction> control the candidate repeat interval detector;
|
|
65 it will consider intervals with at least
|
|
66 <fraction> of matches when shifted by the period;
|
|
67 <fraction> is between 0 and 1 and can be either a
|
|
68 real number or <n>/<d>
|
|
69 (default is 6/7)
|
|
70 --minlength=<length> minimum length of intervals reported, in bp
|
|
71 (default is 20)
|
|
72 --progress=<count> how often to report the sequence we're searching
|
|
73 (default is no progress report)
|
|
74 --allowduplicates process all input sequences
|
|
75 (this is the default)
|
|
76 --noduplicates ignore any input sequence that's the same as an
|
|
77 earlier sequence
|
|
78 --nonearduplicates ignore any input sequence that has the same first
|
|
79 100 bp as an earlier sequence
|
|
80 --nonearduplicate=<length> ignore any input sequence that has the same first
|
|
81 <length> bp as an earlier sequence
|
|
82 --hamming=<count> Don't report candidate repeat intervals that have
|
|
83 more than <count> mismatches
|
|
84 (default is to do no such filtering)
|
|
85 --prefix=<length> Don't report candidate repeat intervals that
|
|
86 start within <length> of the sequence start
|
|
87 (default is to do no such filtering)
|
|
88 --suffix=<length> Don't report candidate repeat intervals that
|
|
89 end within <length> of the sequence end
|
|
90 (default is to do no such filtering)
|
|
91 --subsample=<k>/<n> Process only the <k>th sequence of every group of
|
|
92 <n> sequences; <k> ranges from 1 to <n>
|
|
93 --multipleruns Consider all candidate intervals in a sequence
|
|
94 (default is to consider only the longest)
|
|
95 --partialmotifs Consider microatelites with a partial motif
|
|
96 (default is to consider only whole motifs)
|
|
97 --splitbyvalidity Preprocess sequences, splitting at Ns; this
|
|
98 prevents candidates from including Ns
|
|
99 (default is not to split)
|
|
100 --noflankdisplay Show entire sequence as flanking regions
|
|
101 (this is the default)
|
|
102 --flankdisplay=<length> Limit length of flanking regions shown
|
|
103 --readnamesuffix=<string> Root of suffix to append to read names; e.g. 1
|
|
104 for forward, 2 for reverse; this triggers other
|
|
105 info to be included in the suffix
|
|
106 (default is "1" for fastq; no suffix for fasta)
|
|
107 --head=<number> limit the number of sequences processed
|
|
108 --markend Write a marker line upon completion
|
|
109 (default is not to write a marker)
|
|
110 --help=details Describe the process, and quit"""
|
|
111
|
|
112 if (s == None): exit (message)
|
|
113 else: exit ("%s\n%s" % (s,message))
|
|
114
|
|
115
|
|
116 detailedDescription = """In broad terms, the process works as follows:
|
|
117
|
|
118 (1) Identify intervals that are highly correlated with the interval shifted by
|
|
119 P (the repeat period). These intervals are called "runs" or "candidates".
|
|
120 The level of correlation required is controlled by rateThreshold.
|
|
121 Depending on whether we want to look for more than one microsat, we either
|
|
122 find the longest such run (simple algorithm) or many runs (more complicated
|
|
123 algorithm). The following steps are then performed on each run.
|
|
124
|
|
125 (2) Find the most likely repeat motif in the run. This is done by counting
|
|
126 all kmers (of length P) and choosing the most frequent. If that kmer is
|
|
127 itself covered by a sub-repeat we discard this run. The idea is that we
|
|
128 can ignore a 6-mer like ACGACG because we will find it when we are looking
|
|
129 for 3-mers.
|
|
130
|
|
131 (3) Once we identify the most likely repeat motif, we then modify the
|
|
132 interval, adjusting start and end to find the interval that has the fewest
|
|
133 mismatches vs. a sequence of the motif repeated (hamming distance). Only
|
|
134 whole copies of the motif are considered.
|
|
135
|
|
136 (4) At this point we have a valid microsat interval (in the eyes of the
|
|
137 program). It is subjected to some filtering stages (hamming distance or too
|
|
138 close to an end), and if it satisfies those conditions, it's reported to
|
|
139 the user."""
|
|
140
|
|
141 def main():
|
|
142 global debug
|
|
143
|
|
144 #=== parse the command line ===
|
|
145
|
|
146 inputFilename = None
|
|
147 referenceFileName = None #add by Chen Sun on 02/25
|
|
148 inputFormat = None
|
|
149 repeatPeriods = []
|
|
150 rateThreshold = 6 / 7.0
|
|
151 lengthThreshold = 20
|
|
152 reportProgress = None
|
|
153 discardDuplicates = False
|
|
154 discardNearDuplicates = False
|
|
155 nearDuplicatePrefix = 100
|
|
156 hammingThreshold = 0
|
|
157 prefixThreshold = None
|
|
158 suffixThreshold = None
|
|
159 subsampleK = None
|
|
160 subsampleN = None
|
|
161 reportMultipleRuns = False
|
|
162 allowPartialMotifs = False
|
|
163 splitByValidity = False
|
|
164 flankDisplayLimit = None
|
|
165 readNameSuffix = None
|
|
166 headLimit = None
|
|
167 markEndOfFile = False
|
|
168 additionalInfo = False
|
|
169 debug = []
|
|
170
|
|
171 for arg in argv[1:]:
|
|
172 if (arg == "--fasta"):
|
|
173 inputFormat = "fasta"
|
|
174 elif (arg == "--fastq"):
|
|
175 inputFormat = "fastq"
|
|
176 elif (arg == "--fastq:noquals"):
|
|
177 inputFormat = "fastq:noquals"
|
|
178 elif (arg == "--sam"):
|
|
179 inputFormat = "sam"
|
|
180 elif (arg == "--r"):
|
|
181 additionalInfo = True
|
|
182 elif (arg.startswith("--ref=")):
|
|
183 referenceFileName = arg.split("=",1)[1]
|
|
184 elif (arg.startswith("--period=")):
|
|
185 val = arg.split("=",1)[1]
|
|
186 for period in val.split(","):
|
|
187 if (".." in period):
|
|
188 (lowPeriod,highPeriod) = period.split("..",1)
|
|
189 lowPeriod = int(lowPeriod)
|
|
190 highPeriod = int(highPeriod)
|
|
191 for period in xrange(lowPeriod,highPeriod+1):
|
|
192 repeatPeriods += [period]
|
|
193 else:
|
|
194 repeatPeriods += [int(period)]
|
|
195 elif (arg.startswith("--rate=")):
|
|
196 val = arg.split("=",1)[1]
|
|
197 rateThreshold = float_or_fraction(val)
|
|
198 assert (0.0 < rateThreshold <= 1.0), "%s not a valid rate" % val
|
|
199 elif (arg.startswith("--minlength=")):
|
|
200 val = arg.split("=",1)[1]
|
|
201 lengthThreshold = int(val)
|
|
202 assert (lengthThreshold >= 0)
|
|
203 elif (arg.startswith("--progress=")):
|
|
204 val = arg.split("=",1)[1]
|
|
205 reportProgress = int(val)
|
|
206 elif (arg == "--allowduplicates"):
|
|
207 discardDuplicates = False
|
|
208 discardNearDuplicates = False
|
|
209 elif (arg == "--noduplicates"):
|
|
210 discardDuplicates = True
|
|
211 discardNearDuplicates = False
|
|
212 elif (arg == "--nonearduplicates"):
|
|
213 discardDuplicates = False
|
|
214 discardNearDuplicates = True
|
|
215 elif (arg.startswith("--nonearduplicate=")):
|
|
216 val = arg.split("=",1)[1]
|
|
217 discardDuplicates = False
|
|
218 discardNearDuplicates = True
|
|
219 nearDuplicatePrefix = int(val)
|
|
220 assert (nearDuplicatePrefix > 0)
|
|
221 elif (arg.startswith("--hamming=")):
|
|
222 val = arg.split("=",1)[1]
|
|
223 hammingThreshold = int(val)
|
|
224 assert (hammingThreshold >= 0)
|
|
225 elif (arg.startswith("--prefix=")):
|
|
226 val = arg.split("=",1)[1]
|
|
227 prefixThreshold = int(val)
|
|
228 assert (prefixThreshold >= 0)
|
|
229 elif (arg.startswith("--suffix=")):
|
|
230 val = arg.split("=",1)[1]
|
|
231 suffixThreshold = int(val)
|
|
232 assert (suffixThreshold >= 0)
|
|
233 elif (arg.startswith("--subsample=")):
|
|
234 val = arg.split("=",1)[1]
|
|
235 (k,n) = val.split("/",2)
|
|
236 subsampleK = int(k)
|
|
237 subsampleN = int(n)
|
|
238 assert (0 < subsampleK <= subsampleN)
|
|
239 elif (arg == "--multipleruns"):
|
|
240 reportMultipleRuns = True
|
|
241 elif (arg == "--partialmotifs"):
|
|
242 allowPartialMotifs = True
|
|
243 elif (arg == "--splitbyvalidity"):
|
|
244 splitByValidity = True
|
|
245 elif (arg == "--noflankdisplay"):
|
|
246 flankDisplayLimit = None
|
|
247 elif (arg.startswith("--flankdisplay=")):
|
|
248 val = arg.split("=",1)[1]
|
|
249 flankDisplayLimit = int(val)
|
|
250 assert (flankDisplayLimit >= 0)
|
|
251 elif (arg.startswith("--readnamesuffix")):
|
|
252 readNameSuffix = arg.split("=",1)[1]
|
|
253 elif (arg.startswith("--head=")):
|
|
254 headLimit = int_with_unit(arg.split("=",1)[1])
|
|
255 elif (arg == "--markend"):
|
|
256 markEndOfFile = True
|
|
257 elif (arg == "--help=details"):
|
|
258 exit (detailedDescription)
|
|
259 elif (arg.startswith("--debug=")):
|
|
260 debug += (arg.split("=",1)[1]).split(",")
|
|
261 elif (arg.startswith("--")):
|
|
262 usage("unrecognized option: %s" % arg)
|
|
263 elif (inputFilename == None):
|
|
264 inputFilename = arg
|
|
265 else:
|
|
266 usage("unrecognized option: %s" % arg)
|
|
267
|
|
268 #=== determine periods of interest ===
|
|
269
|
|
270 if (repeatPeriods == []):
|
|
271 usage("you gotta give me a repeat period")
|
|
272
|
|
273 if (additionalInfo == True):
|
|
274 if (referenceFileName == None):
|
|
275 usage("reference file path needed. use --ref=<reference> to indicate")
|
|
276
|
|
277 periodSeed = {}
|
|
278 for period in repeatPeriods:
|
|
279 if (period < 1): usage("period %d is not valid" % period)
|
|
280 periodSeed[period] = True
|
|
281
|
|
282 repeatPeriods = [period for period in periodSeed]
|
|
283 repeatPeriods.sort()
|
|
284
|
|
285 #=== determine input format ===
|
|
286
|
|
287 if (inputFormat == "fasta"): sequence_reader = fasta_sequences
|
|
288 elif (inputFormat == "fastq"): sequence_reader = fastq_sequences
|
|
289 elif (inputFormat == "fastq:noquals"): sequence_reader = fastq_sequences
|
|
290 elif (inputFormat == "sam"): sequence_reader = sam_sequences
|
|
291 elif (inputFilename == None): sequence_reader = fasta_sequences
|
|
292 elif (inputFilename.endswith(".fastq")): sequence_reader = fastq_sequences
|
|
293 elif (inputFilename.endswith(".fq")): sequence_reader = fastq_sequences
|
|
294 elif (inputFilename.endswith(".sam")): sequence_reader = sam_sequences
|
|
295 else: sequence_reader = fasta_sequences
|
|
296
|
|
297 if (inputFilename != None): inputF = file(inputFilename,"rt")
|
|
298 else: inputF = stdin
|
|
299
|
|
300 if (readNameSuffix == None) \
|
|
301 and (sequence_reader == fastq_sequences) \
|
|
302 and (inputFormat != "fastq:noquals"):
|
|
303 readNameSuffix = "1"
|
|
304
|
|
305 #=== process the sequences ===
|
|
306
|
|
307 refSequence = {}
|
|
308 rightName = ""
|
|
309 sequence = ""
|
|
310 if additionalInfo:
|
|
311 firstFasta = True
|
|
312 originalRefF = open(referenceFileName)
|
|
313 for line in originalRefF.readlines():
|
|
314 line = line.replace('\r','')
|
|
315 line = line.replace('\n','')
|
|
316 if line.startswith(">"):
|
|
317 if firstFasta:
|
|
318 firstFasta = False
|
|
319 else:
|
|
320 refSequence[rightName] = sequence
|
|
321 rightName = line[1:]
|
|
322 sequence = ""
|
|
323 continue
|
|
324 sequence += line
|
|
325 originalRefF.close()
|
|
326 refSequence[rightName] = sequence
|
|
327
|
|
328 sequenceSeen = {}
|
|
329
|
|
330 numSequences = 0
|
|
331 for seqInfo in sequence_reader(inputF):
|
|
332 numSequences += 1
|
|
333 if (headLimit != None) and (numSequences > headLimit):
|
|
334 print >>stderr, "limit of %d sequences reached" % headLimit
|
|
335 break
|
|
336
|
|
337 if (sequence_reader == sam_sequences):
|
|
338 #seqName,"".join(seqNucs).upper().translate(nonDnaMap), refName, pre_s, cigar
|
|
339 (name, sequence, refName, pre_s, cigar) = seqInfo
|
|
340 quals = None
|
|
341 elif (sequence_reader == fastq_sequences):
|
|
342 (name,sequence,quals) = seqInfo
|
|
343 if (inputFormat == "fastq:noquals"): quals = None
|
|
344 else:
|
|
345 (name,sequence) = seqInfo
|
|
346 quals = None
|
|
347
|
|
348 if (reportProgress != None) and (numSequences % reportProgress == 0):
|
|
349 print >>stderr, "%s %d" % (name,numSequences)
|
|
350
|
|
351 # if we're subsampling and not interested in this sequence, skip it
|
|
352
|
|
353 if (subsampleN != None):
|
|
354 if ((numSequences-1) % subsampleN != (subsampleK-1)):
|
|
355 continue
|
|
356
|
|
357 # if this sequence is shorter than the length of interest, skip it
|
|
358
|
|
359 seqLen = len(sequence)
|
|
360 if (seqLen < period) or (seqLen < lengthThreshold): continue
|
|
361
|
|
362 # if we're not interested in duplicates and this is one, skip it;
|
|
363 # note that we assume no hash collisions occur, i.e. that all hash
|
|
364 # matches are truly sequence matches
|
|
365
|
|
366 if (discardDuplicates):
|
|
367 h = hash108(sequence)
|
|
368 if (h in sequenceSeen): continue
|
|
369 sequenceSeen[h] = True
|
|
370 elif (discardNearDuplicates):
|
|
371 h = hash108(sequence[:nearDuplicatePrefix])
|
|
372 if (h in sequenceSeen): continue
|
|
373 sequenceSeen[h] = True
|
|
374
|
|
375 # split the sequence into chunks of valid nucleotides
|
|
376
|
|
377 if (splitByValidity):
|
|
378 chunks = [(start,end) for (start,end) in nucleotide_runs(sequence)]
|
|
379 else:
|
|
380 chunks = [(0,len(sequence))]
|
|
381
|
|
382 # evaluate for each period of interest
|
|
383
|
|
384 for period in repeatPeriods:
|
|
385
|
|
386 # operate on each chunk
|
|
387
|
|
388 for (chunkStart,chunkEnd) in chunks:
|
|
389 chunkLen = chunkEnd - chunkStart
|
|
390 if (chunkLen < period) or (chunkLen < lengthThreshold): continue
|
|
391
|
|
392 if ("validity" in debug) or ("correlation" in debug) or ("runs" in debug):
|
|
393 print >>stderr, ">%s_%d_%d" % (name,chunkStart,chunkEnd)
|
|
394
|
|
395 # compute correlation sequence
|
|
396
|
|
397 corr = correlation_sequence(sequence,period,chunkStart,chunkEnd)
|
|
398
|
|
399 if ("correlation" in debug) or ("runs" in debug):
|
|
400 print >>stderr, sequence[chunkStart:chunkEnd]
|
|
401 print >>stderr, corr
|
|
402
|
|
403 # find runs (candidates for being a microsat)
|
|
404
|
|
405 if (reportMultipleRuns):
|
|
406 runs = all_suitable_runs(corr,lengthThreshold-period,rateThreshold, hammingThreshold)
|
|
407 else:
|
|
408 runs = longest_suitable_run(corr,lengthThreshold,rateThreshold)
|
|
409 if (runs == []): continue
|
|
410
|
|
411
|
|
412 if ("runs" in debug):
|
|
413 for (start,end) in runs:
|
|
414 run = [" "] * seqLen
|
|
415 for ix in xrange(start-period,end):
|
|
416 run[ix] = "*"
|
|
417 print >>stderr, "".join(run)
|
|
418
|
|
419 if ("candidates" in debug):
|
|
420 for (start,end) in runs:
|
|
421 print >>stderr, "%s %d %d" % (name,start,end)
|
|
422
|
|
423 # process runs and report those that pass muster
|
|
424
|
|
425 runCount = 0
|
|
426 for (start,end) in runs:
|
|
427 runCount += 1
|
|
428
|
|
429 start = chunkStart + start - period
|
|
430 end = chunkStart + end
|
|
431
|
|
432 (kmer,d,start,end) = find_repeat_element(hammingThreshold, period,sequence,start,end,allowPartials=allowPartialMotifs)
|
|
433 if (kmer == None): continue # (no useful repeat kmer was found)
|
|
434
|
|
435 rptExtent = end - start
|
|
436 prefixLen = start
|
|
437 suffixLen = seqLen - end
|
|
438 if (rptExtent <= period): continue
|
|
439 if (hammingThreshold != None) and (d > hammingThreshold): continue
|
|
440 if (prefixThreshold != None) and (prefixLen < prefixThreshold): continue
|
|
441 if (suffixThreshold != None) and (suffixLen < suffixThreshold): continue
|
|
442
|
|
443 if (flankDisplayLimit == None):
|
|
444 seq = sequence[:start] \
|
|
445 + sequence[start:end].lower() \
|
|
446 + sequence[end:]
|
|
447 else:
|
|
448 seq = sequence[max(chunkStart,start-flankDisplayLimit):start] \
|
|
449 + sequence[start:end].lower() \
|
|
450 + sequence[end:min(chunkEnd,end+flankDisplayLimit)]
|
|
451 reportName = name
|
|
452 if (readNameSuffix != None):
|
|
453 reportName += "_"+readNameSuffix+"_per"+str(period)+"_"+str(runCount)
|
|
454 if (quals == None or quals == "." or quals == "\t."): quals = "\t."
|
|
455 else: quals = "\t" + quals
|
|
456 if not additionalInfo:
|
|
457 print "%d\t%d\t%d\t%s\t%d\t%s\t%s%s" \
|
|
458 % (rptExtent,prefixLen,suffixLen,kmer,d,reportName,seq,quals)
|
|
459 else:
|
|
460 #pre_e = pre_s + prefixLen - 1
|
|
461 refPoint = pre_s
|
|
462 donorPoint = 0
|
|
463
|
|
464 donorBeforeStart = prefixLen - 1 #pre_e
|
|
465 donorMicroStart = prefixLen #tr_s
|
|
466 donorMicroEnd = donorMicroStart + rptExtent - 1 #tr_e
|
|
467 donorAfterMicro = donorMicroEnd + 1 #suf_s
|
|
468 donorEnd = len(seq) - 1 #suf_e
|
|
469
|
|
470 set_pre_e = False
|
|
471 set_tr_s = False
|
|
472 set_tr_e = False
|
|
473 set_suf_s = False
|
|
474 set_suf_e = False
|
|
475
|
|
476 pre_e = 0
|
|
477 tr_s = 0
|
|
478 tr_e = 0
|
|
479 suf_s = 0
|
|
480 suf_e = 0
|
|
481
|
|
482 matchList = re.findall('(\d+)([IDM])', cigar)
|
|
483 unCognitiveCigar = False
|
|
484 for matchN, matchType in matchList:
|
|
485 matchNum = int(matchN)
|
|
486 if matchType == "M":
|
|
487 donorPoint = donorPoint + matchNum
|
|
488 refPoint = refPoint + matchNum
|
|
489 elif matchType == "D":
|
|
490 refPoint = refPoint + matchNum
|
|
491 continue
|
|
492 elif matchType == "I":
|
|
493 donorPoint = donorPoint + matchNum
|
|
494 else:
|
|
495 unCognitiveCigar = True
|
|
496 break
|
|
497
|
|
498 if not set_pre_e:
|
|
499 if donorPoint >= donorBeforeStart:
|
|
500 pre_e = refPoint - (donorPoint - donorBeforeStart)
|
|
501 set_pre_e = True
|
|
502 else:
|
|
503 continue
|
|
504
|
|
505 if not set_tr_s:
|
|
506 if donorPoint >= donorMicroStart:
|
|
507 tr_s = refPoint - (donorPoint - donorMicroStart)
|
|
508 set_tr_s = True
|
|
509 else:
|
|
510 continue
|
|
511
|
|
512 if not set_tr_e:
|
|
513 if donorPoint >= donorMicroEnd:
|
|
514 tr_e = refPoint - (donorPoint - donorMicroEnd)
|
|
515 set_tr_e = True
|
|
516 else:
|
|
517 continue
|
|
518
|
|
519 if not set_suf_s:
|
|
520 if donorPoint >= donorAfterMicro:
|
|
521 suf_s = refPoint - (donorPoint - donorAfterMicro)
|
|
522 set_suf_s = True
|
|
523 else:
|
|
524 continue
|
|
525
|
|
526 if not set_suf_e:
|
|
527 if donorPoint >= donorEnd:
|
|
528 suf_e = refPoint - (donorPoint - donorEnd)
|
|
529 set_suf_e = True
|
|
530 else:
|
|
531 continue
|
|
532
|
|
533 if unCognitiveCigar:
|
|
534 break
|
|
535 tr_len = tr_e - tr_s + 1
|
|
536
|
|
537 if refName not in refSequence:
|
|
538 tr_ref_seq = "."
|
|
539 else:
|
|
540 if refSequence[refName] == "":
|
|
541 tr_ref_seq = "."
|
|
542 elif len(refSequence[refName]) <= tr_e:
|
|
543 tr_ref_seq = "."
|
|
544 else:
|
|
545 tr_ref_seq = refSequence[refName][tr_s:tr_e+1]
|
|
546
|
|
547 pre_e += 1
|
|
548 tr_e += 1
|
|
549 suf_e += 1
|
|
550 print "%d\t%d\t%d\t%s\t%d\t%s\t%s%s\t%s\t%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%s" \
|
|
551 % (rptExtent,prefixLen,suffixLen,kmer,d,reportName,seq,quals,reportName,refName,pre_s,pre_e,tr_s,tr_e,suf_s,suf_e,tr_len,tr_ref_seq)
|
|
552
|
|
553 if (markEndOfFile):
|
|
554 print "# microsat_snoop end-of-file"
|
|
555
|
|
556 if (inputF != stdin):
|
|
557 inputF.close()
|
|
558
|
|
559 # non_negative_intervals
|
|
560 # find intervals with exactly + and no -
|
|
561 # from string like this : +++++++++---+++++++++
|
|
562 def non_negative_intervals(seq, minLength=None):
|
|
563
|
|
564 start = -1
|
|
565 end = -1
|
|
566 firstPlus = 1
|
|
567 #print seq
|
|
568 for ix in range(len(seq)): # for every char in seq
|
|
569 ch = seq[ix]
|
|
570 if(ch == "+"):
|
|
571 if(firstPlus):
|
|
572 firstPlus = 0
|
|
573 start = ix
|
|
574 else:
|
|
575 continue
|
|
576 elif(ch == "-"):
|
|
577 if(start >= 0):
|
|
578 end = ix-1
|
|
579 if((end - start + 1) >= minLength):
|
|
580 yield (start,end+1)
|
|
581 start = -1
|
|
582 firstPlus = 1
|
|
583 if(start > 0):
|
|
584 if((ix - start + 1) >= minLength):
|
|
585 yield (start, ix+1)
|
|
586
|
|
587
|
|
588 ###################################################################
|
|
589 # modified by Chen Sun on 7/11/2014
|
|
590 # We do not want other modules, so parse these functions inside
|
|
591 #
|
|
592 ###################################################################
|
|
593
|
|
594 # parse a string of the form {positives}/{positives_and_neutrals}
|
|
595
|
|
596 def parse_spec(s):
|
|
597 if ("/" not in s): raise ValueError
|
|
598 (n,d) = s.split("/",1)
|
|
599 if (not n.startswith("{")) or (not n.endswith("}")): raise ValueError
|
|
600 if (not d.startswith("{")) or (not d.endswith("}")): raise ValueError
|
|
601
|
|
602 positives = n[1:-1]
|
|
603 d = d[1:-1]
|
|
604
|
|
605 for ch in positives:
|
|
606 if (ch not in d): raise ValueError
|
|
607
|
|
608 neutrals = [ch for ch in d if (ch not in positives)]
|
|
609 return (positives,neutrals)
|
|
610
|
|
611
|
|
612 # convert a string to a number, allowing fractions
|
|
613
|
|
614 def float_or_fraction(s):
|
|
615 if ("/" in s):
|
|
616 (numer,denom) = s.split("/",1)
|
|
617 return float(numer)/float(denom)
|
|
618 else:
|
|
619 return float(s)
|
|
620
|
|
621
|
|
622 # dense_intervals--
|
|
623 # Find all non-overlapping runs with a good enough rate (of positives), and
|
|
624 # which meet our length threshold.
|
|
625 #
|
|
626 # The algorithm used is adapted from Zhang, Berman, Miller, "Post-processing
|
|
627 # long pairwise alignments", Bioinformatics Vol. 15 no. 12 1999.
|
|
628 #
|
|
629 # $$$ we use the denominator as the threshold, but we really should use the
|
|
630 # $$$ .. numerator, comparing it to minLength*rate
|
|
631
|
|
632 def dense_intervals(seq,rate,positives,neutrals,blockers="",minLength=None):
|
|
633
|
|
634 if (blockers == None):
|
|
635 blockers = "".join([chr(n) for n in range(1,256)
|
|
636 if (chr(n) not in positives)
|
|
637 and (chr(n) not in neutrals)])
|
|
638
|
|
639 stackLeft = [None] # stack with each entry containing five
|
|
640 stackRight = [None] # .. elements; note that entry zero is not
|
|
641 stackLeftScore = [None] # .. used
|
|
642 stackRightScore = [None]
|
|
643 stackLower = [None]
|
|
644 top = 0
|
|
645 score = 0
|
|
646
|
|
647 for ix in range(len(seq)):
|
|
648 ch = seq[ix]
|
|
649 if (ch in blockers):
|
|
650 # emit intervals
|
|
651
|
|
652 for sp in range(1,top+1):
|
|
653 left = stackLeft [sp] + 1
|
|
654 right = stackRight[sp]
|
|
655
|
|
656 while (left < right) and (seq[left] not in positives): left += 1
|
|
657 while (right > left) and (seq[right] not in positives): right -= 1
|
|
658
|
|
659 right += 1
|
|
660 if (minLength == None) or (right - left >= minLength):
|
|
661 yield (left,right)
|
|
662
|
|
663 #empty stack
|
|
664
|
|
665 stackLeft = [None]
|
|
666 stackRight = [None]
|
|
667 stackLeftScore = [None]
|
|
668 stackRightScore = [None]
|
|
669 stackLower = [None]
|
|
670 top = 0
|
|
671 score = 0
|
|
672 continue
|
|
673
|
|
674 if (ch in positives): weight = 1-rate
|
|
675 elif (ch in neutrals): weight = -rate
|
|
676 else: raise ValueError
|
|
677
|
|
678 score += weight
|
|
679 #if ("algorithm" in debug):
|
|
680 # print >>sys.stderr, "%3d: %c %5.2f" % (ix, ch, score),
|
|
681
|
|
682 if (weight < 0):
|
|
683 #if ("algorithm" in debug):
|
|
684 # print >>sys.stderr
|
|
685 continue
|
|
686
|
|
687 if (top > 0) and (stackRight[top] == ix-1):
|
|
688 # add this site to the interval on top of the stack
|
|
689
|
|
690 stackRight [top] = ix
|
|
691 stackRightScore[top] = score
|
|
692
|
|
693 #if ("algorithm" in debug):
|
|
694 # print >>sys.stderr, \
|
|
695 # " extending [%d] %d-%d %4.1f %4.1f" \
|
|
696 # % (top,
|
|
697 # stackLeft [top], stackRight [top],
|
|
698 # stackLeftScore[top], stackRightScore[top]),
|
|
699
|
|
700 else:
|
|
701 # create a one site interval
|
|
702
|
|
703 top += 1
|
|
704 if (top >= len(stackLeft)):
|
|
705 stackLeft += [None]
|
|
706 stackRight += [None]
|
|
707 stackLeftScore += [None]
|
|
708 stackRightScore += [None]
|
|
709 stackLower += [None]
|
|
710
|
|
711 stackLeft [top] = ix - 1
|
|
712 stackLeftScore [top] = score - weight
|
|
713 stackRight [top] = ix
|
|
714 stackRightScore[top] = score
|
|
715 stackLower [top] = top - 1
|
|
716
|
|
717 while (stackLower[top] > 0) \
|
|
718 and (stackLeftScore[stackLower[top]] > stackLeftScore[top]):
|
|
719 stackLower[top] = stackLower[stackLower[top]]
|
|
720
|
|
721 #if ("algorithm" in debug):
|
|
722 # print >>sys.stderr, \
|
|
723 # " creating [%d] %d-%d %4.1f %4.1f -> %d" \
|
|
724 # % (top,
|
|
725 # stackLeft [top], stackRight [top],
|
|
726 # stackLeftScore[top], stackRightScore[top],
|
|
727 # stackLower [top]),
|
|
728
|
|
729 # merge intervals; if there is a previous interval with a no-higher
|
|
730 # left score and no-higher right score, merge this interval (and all
|
|
731 # intervening ones) into that one
|
|
732
|
|
733 while (top > 1) \
|
|
734 and (stackLower[top] > 0) \
|
|
735 and (stackRightScore[stackLower[top]] <= stackRightScore[top]):
|
|
736 stackRight [stackLower[top]] = stackRight [top]
|
|
737 stackRightScore[stackLower[top]] = stackRightScore[top]
|
|
738 top = stackLower[top]
|
|
739
|
|
740 #if ("algorithm" in debug):
|
|
741 # print >>sys.stderr, \
|
|
742 # "\n%*s merging [%d] %d-%d %4.1f %4.1f" \
|
|
743 # % (13, "", top,
|
|
744 # stackLeft[top], stackRight [top],
|
|
745 # stackLeftScore[top], stackRightScore[top]),
|
|
746
|
|
747 #if ("algorithm" in debug):
|
|
748 # print >>sys.stderr
|
|
749
|
|
750 # emit intervals
|
|
751
|
|
752 for sp in range(1,top+1):
|
|
753 left = stackLeft [sp] + 1
|
|
754 right = stackRight[sp]
|
|
755
|
|
756 while (left < right) and (seq[left] not in positives): left += 1
|
|
757 while (right > left) and (seq[right] not in positives): right -= 1
|
|
758
|
|
759 right += 1
|
|
760 if (minLength == None) or (right - left >= minLength):
|
|
761 yield (left,right)
|
|
762
|
|
763
|
|
764 ###################################################################
|
|
765 # modified by Chen Sun on 7/11/2014
|
|
766 #
|
|
767 ###################################################################
|
|
768
|
|
769 # correlation_sequence--
|
|
770 # Compute the correlation sequence for a given period. This is a sequence
|
|
771 # of + and - indicating whether the base at a given position matches the one
|
|
772 # P positions earlier (where P is the period). The first P positions are
|
|
773 # blank. Positions with single character runs longer than the period are
|
|
774 # considered as non-matches, unless the period is 1.
|
|
775
|
|
776 def correlation_sequence(sequence,period,start=None,end=None):
|
|
777 if (start == None): start = 0
|
|
778 if (end == None): end = len(sequence)
|
|
779
|
|
780 prevCh = sequence[start]
|
|
781 run = 1
|
|
782 for ix in xrange(start+1,start+period):
|
|
783 ch = sequence[ix]
|
|
784 if (ch != prevCh): run = 1
|
|
785 else: run += 1
|
|
786 prevCh = ch
|
|
787
|
|
788 corr = [" "] * period
|
|
789 for ix in xrange(start+period,end):
|
|
790 rptCh = sequence[ix-period]
|
|
791 ch = sequence[ix]
|
|
792 if (ch != prevCh): run = 1
|
|
793 else: run += 1
|
|
794 if (ch in "ACGT") \
|
|
795 and (ch == rptCh) \
|
|
796 and ((period == 1) or (run < period)):
|
|
797 corr += ["+"]
|
|
798 else:
|
|
799 corr += ["-"]
|
|
800 prevCh = ch
|
|
801
|
|
802 return "".join(corr)
|
|
803
|
|
804
|
|
805 # longest_suitable_run--
|
|
806 # Find longest run with a good enough rate (of positives).
|
|
807 #
|
|
808 # We score a "+" as 1-r and anything else as -r. This is based on the fol-
|
|
809 # lowing derivation (p is the number of "+"s, n is the number of non-"+"s):
|
|
810 # p/(p+n) >= r
|
|
811 # ==> p >= rp + rn
|
|
812 # ==> (1-r)p - rn >= 0
|
|
813 #
|
|
814 # We adapt an algorithm from "Programming Pearls", pg. 81 (2000 printing).
|
|
815 #
|
|
816 # $$$ we use the denominator as the threshold, but we really should use the
|
|
817 # $$$ .. numerator, comparing it to minLength*rate
|
|
818 #
|
|
819 # $$$ this needs to account for $$$ this situation:
|
|
820 # $$$ sequence: ACGACGACGACGTTATTATTATTA
|
|
821 # $$$ matches: +++++++++---+++++++++
|
|
822 # $$$ this is currently considered to be one interval (if rate <= 6/7), but it
|
|
823 # $$$ ought to be two; we can't just post-process, though, because some other
|
|
824 # $$$ interval might be longer than the longest half of this; maybe what we
|
|
825 # $$$ need to do is consider matches at distances -P and -2P, or if we match
|
|
826 # $$$ -P but that itself was a mismatch, we should carry the mismatch forward
|
|
827
|
|
828 def longest_suitable_run(seq,minLength,rate):
|
|
829 maxEndingHere = 0
|
|
830 maxSoFar = 0
|
|
831 start = None
|
|
832
|
|
833 for ix in xrange(len(seq)):
|
|
834 if (seq[ix] == "+"): s = 1-rate
|
|
835 else: s = -rate
|
|
836
|
|
837 if (maxEndingHere+s < 0):
|
|
838 maxEndingHere = 0
|
|
839 block = ix
|
|
840 else:
|
|
841 maxEndingHere += s
|
|
842 if (maxEndingHere >= maxSoFar):
|
|
843 maxSoFar = maxEndingHere
|
|
844 start = block + 1
|
|
845 end = ix + 1
|
|
846
|
|
847 if (start == None) or (end - start < minLength):
|
|
848 return []
|
|
849 else:
|
|
850 return [(start,end)]
|
|
851
|
|
852
|
|
853 # all_suitable_runs--
|
|
854 # Find all non-overlapping runs with a good enough rate (of positives), and
|
|
855 # which meet our length threshold.
|
|
856 # $$$ this needs to post-process the intervals, splitting them to account for
|
|
857 # $$$ this situation:
|
|
858 # $$$ sequence: ACGACGACGACGTTATTATTATTA
|
|
859 # $$$ matches: +++++++++---+++++++++
|
|
860 # $$$ this is currently reported as one interval (if rate <= 6/7), but it
|
|
861 # $$$ ought to be two
|
|
862
|
|
863 def all_suitable_runs(seq,minCorrLength,rate, hammingThreshold):
|
|
864
|
|
865 ################################################################
|
|
866 # modified by Chen Sun on 07/11/2014
|
|
867 #
|
|
868 ################################################################
|
|
869
|
|
870 if hammingThreshold > 0:
|
|
871 return [(start,end) for (start,end) in dense_intervals(seq,rate,"+","-",blockers=None,minLength=minCorrLength)]
|
|
872 elif hammingThreshold == 0:
|
|
873 return [(start,end) for (start,end) in non_negative_intervals(seq, minLength=minCorrLength)]
|
|
874
|
|
875
|
|
876 # find_repeat_element--
|
|
877 # Find the most plausible repeat element for a run, and nudge the ends of
|
|
878 # the run if needed. Note that we will not consider kmers that represent
|
|
879 # shorter repeats. For example, we won't report ACTACT as a 6-mer since we
|
|
880 # consider this to have a shorter period than 6.
|
|
881
|
|
882 def find_repeat_element(hammingThreshold, period,seq,start,end,allowPartials=False):
|
|
883
|
|
884 if hammingThreshold > 0:
|
|
885 (kmer,bestD,bestStart,bestEnd) = find_hamming_repeat_element(period,seq,start,end,allowPartials)
|
|
886 return (kmer,bestD,bestStart,bestEnd)
|
|
887 # count the number of occurences of each k-mer; note that we can't
|
|
888 # reject kmers containing smaller repeats yet, since for a sequence like
|
|
889 # ACACACACACAAACACACACACACACACAC we must first discover ACACAC as the best
|
|
890 # 6-mer, and THEN reject it; if we reject ACACAC while counting, we'd end
|
|
891 # up reporting something like ACACAA as the best motif
|
|
892
|
|
893 if ("element" in debug):
|
|
894 print >>stderr, "find_repeat_element(%d,%d,%d)" % (period,start,end)
|
|
895
|
|
896 if ("partial" in debug):
|
|
897 print period, seq, start, end, allowPartials;
|
|
898 print seq[start:end]
|
|
899
|
|
900 kmerToCount = {}
|
|
901 kmerToFirst = {}
|
|
902 for ix in xrange(start,end-(period-1)):
|
|
903 kmer = seq[ix:ix+period]
|
|
904 if ("N" in kmer): continue
|
|
905 if (kmer not in kmerToCount):
|
|
906 kmerToCount[kmer] = 1
|
|
907 kmerToFirst[kmer] = ix
|
|
908 else:
|
|
909 kmerToCount[kmer] += 1
|
|
910 #if ("element" in debug):
|
|
911 # print >>stderr, " %d: %s" % (ix,kmer)
|
|
912
|
|
913 # choose the best k-mer; this is simply the most frequently occurring one,
|
|
914 # with ties broken by whichever one came first
|
|
915
|
|
916 kmers = [(-kmerToCount[kmer],kmerToFirst[kmer],kmer) for kmer in kmerToCount]
|
|
917 if (kmers == []): return (None,None,start,end)
|
|
918 kmers.sort()
|
|
919
|
|
920 if ("element" in debug):
|
|
921 for (count,first,kmer) in kmers:
|
|
922 print >>stderr, " %s: %d" % (kmer,-count)
|
|
923
|
|
924 (count,first,kmer) = kmers[0]
|
|
925 if (contains_repeat(kmer)): return (None,None,start,end)
|
|
926
|
|
927 # determine the hamming distance between the run and a simple repeat, for
|
|
928 # each "plausible" start and end; we compute the distance for each such
|
|
929 # interval, and choose the one with the lowest hamming distance; ties are
|
|
930 # broken in a deterministic-but-unspecified manner
|
|
931
|
|
932 bestD = bestStart = bestEnd = None
|
|
933 ###################################################################################
|
|
934 # modified by Chen Sun(cxs1031@cse.psu.edu) on 10/18/2013
|
|
935 # since we do not allow hamming_distance > 0, which means we do not allow mutation,
|
|
936 # we do not need this section to produce bestStart and End
|
|
937 ###################################################################################
|
|
938
|
|
939 #for (s,e) in plausible_intervals(start,end,period,len(seq),allowPartials=allowPartials):
|
|
940 # d = hamming_distance(seq,s,e,kmer)
|
|
941 # if (d == None): continue
|
|
942 # if (bestD == None) or (d <= bestD):
|
|
943 # (bestD,bestStart,bestEnd) = (d,s,e)
|
|
944
|
|
945
|
|
946
|
|
947 bestStart = start
|
|
948
|
|
949 if(allowPartials):
|
|
950 bestEnd = end
|
|
951 elif(not allowPartials):
|
|
952 bestEnd = start
|
|
953 pattern = seq[start:start+period]
|
|
954 if ("partial" in debug):
|
|
955 print "kmer:", kmer
|
|
956 if(pattern != kmer):
|
|
957 print "pattern:", pattern
|
|
958
|
|
959 while(bestEnd <= end-period):
|
|
960 bestEnd += period
|
|
961
|
|
962 # bestD will always be 0, as we do not allow mutation
|
|
963 bestD = 0
|
|
964
|
|
965 if ("partial" in debug):
|
|
966 print bestD, bestStart, bestEnd
|
|
967
|
|
968 ###################################################################################
|
|
969 # modified by Chen Sun(cxs1031@cse.psu.edu) on 10/10
|
|
970 #
|
|
971 ###################################################################################
|
|
972 return (kmer,bestD,bestStart,bestEnd)
|
|
973
|
|
974
|
|
975 def find_hamming_repeat_element(period,seq,start,end,allowPartials=False):
|
|
976
|
|
977 # count the number of occurences of each k-mer; note that we can't
|
|
978 # reject kmers containing smaller repeats yet, since for a sequence like
|
|
979 # ACACACACACAAACACACACACACACACAC we must first discover ACACAC as the best
|
|
980 # 6-mer, and THEN reject it; if we reject ACACAC while counting, we'd end
|
|
981 # up reporting something like ACACAA as the best motif
|
|
982
|
|
983 if ("element" in debug):
|
|
984 print >>stderr, "find_repeat_element(%d,%d,%d)" % (period,start,end)
|
|
985
|
|
986 kmerToCount = {}
|
|
987 kmerToFirst = {}
|
|
988 for ix in xrange(start,end-(period-1)):
|
|
989 kmer = seq[ix:ix+period]
|
|
990 if ("N" in kmer): continue
|
|
991 if (kmer not in kmerToCount):
|
|
992 kmerToCount[kmer] = 1
|
|
993 kmerToFirst[kmer] = ix
|
|
994 else:
|
|
995 kmerToCount[kmer] += 1
|
|
996 #if ("element" in debug):
|
|
997 # print >>stderr, " %d: %s" % (ix,kmer)
|
|
998
|
|
999 # choose the best k-mer; this is simply the most frequently occurring one,
|
|
1000 # with ties broken by whichever one came first
|
|
1001
|
|
1002 kmers = [(-kmerToCount[kmer],kmerToFirst[kmer],kmer) for kmer in kmerToCount]
|
|
1003 if (kmers == []): return (None,None,start,end)
|
|
1004 kmers.sort()
|
|
1005
|
|
1006 if ("element" in debug):
|
|
1007 for (count,first,kmer) in kmers:
|
|
1008 print >>stderr, " %s: %d" % (kmer,-count)
|
|
1009
|
|
1010 (count,first,kmer) = kmers[0]
|
|
1011 if (contains_repeat(kmer)): return (None,None,start,end)
|
|
1012
|
|
1013 # determine the hamming distance between the run and a simple repeat, for
|
|
1014 # each "plausible" start and end; we compute the distance for each such
|
|
1015 # interval, and choose the one with the lowest hamming distance; ties are
|
|
1016 # broken in a deterministic-but-unspecified manner
|
|
1017
|
|
1018 bestD = bestStart = bestEnd = None
|
|
1019
|
|
1020 for (s,e) in plausible_intervals(start,end,period,len(seq),allowPartials=allowPartials):
|
|
1021 d = hamming_distance(seq,s,e,kmer)
|
|
1022 if (d == None): continue
|
|
1023 if (bestD == None) or (d <= bestD):
|
|
1024 (bestD,bestStart,bestEnd) = (d,s,e)
|
|
1025
|
|
1026 return (kmer,bestD,bestStart,bestEnd)
|
|
1027
|
|
1028 # plausible_intervals--
|
|
1029 # Yield all plausible intervals intersecting with a run. We generate all
|
|
1030 # starts within P bp of the run's start. For each of these, we either (a) try
|
|
1031 # all ends within P bp of run's end, or (b) trim the new interval to a whole
|
|
1032 # multiple of the period, and report this short interval and the longer
|
|
1033 # interval with one more period appended. Case (a) allows partial motifs,
|
|
1034 # while case (b) only allows whole motifs.
|
|
1035
|
|
1036 def plausible_intervals(start,end,period,seqLen,allowPartials=False):
|
|
1037
|
|
1038 # generate intervals that allow a partial copy of the motif
|
|
1039
|
|
1040 if (allowPartials):
|
|
1041 for candStart in xrange(start-(period-1),start+period):
|
|
1042 if (candStart < 0): continue
|
|
1043 for candEnd in xrange(end-(period-1),end+period):
|
|
1044 if (candEnd > seqLen): continue
|
|
1045 if (candEnd <= candStart+period): continue
|
|
1046 yield (candStart,candEnd)
|
|
1047
|
|
1048 # -OR- generate intervals that allow only whole copies of the motif
|
|
1049
|
|
1050 else:
|
|
1051 for candStart in xrange(start-(period-1),start+period):
|
|
1052 if (candStart < 0): continue
|
|
1053 candEnd = candStart + ((end-candStart)/period)*period
|
|
1054 yield (candStart,candEnd)
|
|
1055 candEnd += period
|
|
1056 if (candEnd <= seqLen): yield (candStart,candEnd)
|
|
1057
|
|
1058
|
|
1059 # hamming_distance--
|
|
1060 # Determine the hamming distance between the run and a simple repeat.
|
|
1061 # $$$ improve this by allowing gaps, and stopping when we reach a threshold
|
|
1062
|
|
1063 kmerToDiffs = {} # (this is used for memo-ization)
|
|
1064
|
|
1065 def hamming_distance(seq,start,end,kmer):
|
|
1066 period = len(kmer)
|
|
1067 if (end < start + period): return None
|
|
1068
|
|
1069 wholeEnd = start + ((end-start)/period)*period
|
|
1070
|
|
1071 if (kmer not in kmerToDiffs):
|
|
1072 kmerToDiffs[kmer] = { kmer:0 }
|
|
1073
|
|
1074 d = 0
|
|
1075 for ix in xrange(start,wholeEnd,period):
|
|
1076 qmer = seq[ix:ix+period] # same size as the kmer motif
|
|
1077 if (qmer in kmerToDiffs[kmer]):
|
|
1078 d += kmerToDiffs[kmer][qmer]
|
|
1079 continue
|
|
1080 diffs = 0
|
|
1081 for iy in xrange(0,period):
|
|
1082 if (qmer[iy] != kmer[iy]): diffs += 1
|
|
1083 kmerToDiffs[kmer][qmer] = diffs
|
|
1084 d += diffs
|
|
1085
|
|
1086 if (end > wholeEnd):
|
|
1087 qmer = seq[wholeEnd:end] # shorter than the kmer motif
|
|
1088 if (qmer in kmerToDiffs[kmer]):
|
|
1089 d += kmerToDiffs[kmer][qmer]
|
|
1090 else:
|
|
1091 diffs = 0
|
|
1092 for iy in xrange(0,len(qmer)):
|
|
1093 if (qmer[iy] != kmer[iy]): diffs += 1
|
|
1094 kmerToDiffs[kmer][qmer] = diffs
|
|
1095 d += diffs
|
|
1096
|
|
1097 return d
|
|
1098
|
|
1099
|
|
1100 # fasta_sequences--
|
|
1101 # Read the fasta sequences from a file. Note that we convert to upper case,
|
|
1102 # and convert any letter other than ACGT to N.
|
|
1103
|
|
1104 nonDnaMap = maketrans("BDEFHIJKLMOPQRSUVWXYZ","NNNNNNNNNNNNNNNNNNNNN")
|
|
1105
|
|
1106 def fasta_sequences(f):
|
|
1107 seqName = None
|
|
1108 seqNucs = None
|
|
1109
|
|
1110 for line in f:
|
|
1111 line = line.strip()
|
|
1112 if (line.startswith(">")):
|
|
1113 if (seqName != None):
|
|
1114 yield (seqName,"".join(seqNucs))
|
|
1115 seqName = sequence_name(line)
|
|
1116 seqNucs = []
|
|
1117 elif (seqName == None):
|
|
1118 assert (False), "first sequence has no header"
|
|
1119 else:
|
|
1120 seqNucs += [line]
|
|
1121
|
|
1122 if (seqName != None):
|
|
1123 yield (seqName,"".join(seqNucs).upper().translate(nonDnaMap))
|
|
1124
|
|
1125
|
|
1126 # fastq_sequences--
|
|
1127 # Read the fastq sequences from a file. Note that we convert to upper case,
|
|
1128 # and convert any letter other than ACGT to N.
|
|
1129
|
|
1130 def fastq_sequences(f):
|
|
1131 lineNum = 0
|
|
1132 for line in f:
|
|
1133 lineNum += 1
|
|
1134 line = line.strip()
|
|
1135
|
|
1136 if (lineNum % 4 == 1):
|
|
1137 assert (line.startswith("@")), \
|
|
1138 "bad read name at line %d" % lineNum
|
|
1139 seqName = line[1:]
|
|
1140 continue
|
|
1141
|
|
1142 if (lineNum % 4 == 2):
|
|
1143 seqNucs = line
|
|
1144 continue
|
|
1145
|
|
1146 if (lineNum % 4 == 3):
|
|
1147 assert (line.startswith("+")), \
|
|
1148 "can't understand line %d:\n%s" % (lineNum,line)
|
|
1149 continue
|
|
1150
|
|
1151 quals = line
|
|
1152 assert (len(quals) == len(seqNucs)), \
|
|
1153 "length mismatch read vs. qualities at line %d" % lineNum
|
|
1154 yield (seqName,"".join(seqNucs).upper().translate(nonDnaMap),quals)
|
|
1155
|
|
1156 assert (lineNum % 4 == 0), \
|
|
1157 "incomplete read at end of file"
|
|
1158
|
|
1159 def sam_sequences(f):
|
|
1160 lineNum = 0
|
|
1161 for line in f:
|
|
1162 lineNum += 1
|
|
1163 line = line.strip()
|
|
1164
|
|
1165 if line.startswith("@"):
|
|
1166 continue
|
|
1167
|
|
1168 columns = line.split("\t")
|
|
1169 seqName = columns[0]
|
|
1170 refName = columns[2]
|
|
1171 pre_s = int(columns[3]) - 1
|
|
1172 cigar = columns[5]
|
|
1173 seqNucs = columns[9]
|
|
1174
|
|
1175 yield (seqName,"".join(seqNucs).upper().translate(nonDnaMap), refName, pre_s, cigar)
|
|
1176
|
|
1177 # sequence_name--
|
|
1178 # Extract the sequence name from a fasta header.
|
|
1179 # $$$ this may need to be improved $$$
|
|
1180
|
|
1181 def sequence_name(s):
|
|
1182 s = s[1:].strip()
|
|
1183 if (s == ""): return ""
|
|
1184 else: return s.split()[0]
|
|
1185
|
|
1186
|
|
1187 # nucleotide_runs--
|
|
1188 # Yield (start,end) for all runs of valid nucleotides in a sequence.
|
|
1189
|
|
1190 def nucleotide_runs(s):
|
|
1191 runs = []
|
|
1192 start = None
|
|
1193 for (ix,nuc) in enumerate(s):
|
|
1194 if (nuc in "ACGT"):
|
|
1195 if (start == None):
|
|
1196 start = ix
|
|
1197 else:
|
|
1198 if (start != None):
|
|
1199 yield (start,ix)
|
|
1200 start = None
|
|
1201
|
|
1202 if (start != None): yield (start,len(s))
|
|
1203
|
|
1204
|
|
1205 # contains_repeat--
|
|
1206 # Determine whether a short sequence contains a repeated element, such as a
|
|
1207 # 6-mer containing a repeated 2-mer (ACACAC) or 3-mer (ACTACT). The repeat
|
|
1208 # must cover the entire sequence, without mismatches.
|
|
1209
|
|
1210 def contains_repeat(kmer):
|
|
1211 kmerLength = len(kmer)
|
|
1212 hasRepeat = False
|
|
1213 rptLen = 1
|
|
1214 while (not hasRepeat) and (2 * rptLen <= kmerLength):
|
|
1215 if (kmerLength % rptLen != 0):
|
|
1216 rptLen += 1
|
|
1217 continue
|
|
1218 isRepeat = True
|
|
1219 for i in xrange(rptLen,kmerLength,rptLen):
|
|
1220 if (kmer[i:i+rptLen] != kmer[:rptLen]):
|
|
1221 isRepeat = False
|
|
1222 break
|
|
1223 if (isRepeat):
|
|
1224 hasRepeat = True
|
|
1225 break
|
|
1226 rptLen += 1
|
|
1227 return hasRepeat
|
|
1228
|
|
1229
|
|
1230 # hash108--
|
|
1231 # Return a 108-bit hash "value" of a string
|
|
1232
|
|
1233 def hash108(s):
|
|
1234 m = md5_new()
|
|
1235 m.update(s)
|
|
1236 return m.hexdigest()[:27]
|
|
1237
|
|
1238
|
|
1239 # float_or_fraction--
|
|
1240 # Convert a string to a number, allowing fractions
|
|
1241
|
|
1242 def float_or_fraction(s):
|
|
1243 if ("/" in s):
|
|
1244 (numer,denom) = s.split("/",1)
|
|
1245 return float(numer)/float(denom)
|
|
1246 else:
|
|
1247 return float(s)
|
|
1248
|
|
1249
|
|
1250 # int_with_unit--
|
|
1251 # Parse a string as an integer, allowing unit suffixes
|
|
1252
|
|
1253 def int_with_unit(s):
|
|
1254 if (s.endswith("K")):
|
|
1255 multiplier = 1000
|
|
1256 s = s[:-1]
|
|
1257 elif (s.endswith("M")):
|
|
1258 multiplier = 1000 * 1000
|
|
1259 s = s[:-1]
|
|
1260 elif (s.endswith("G")):
|
|
1261 multiplier = 1000 * 1000 * 1000
|
|
1262 s = s[:-1]
|
|
1263 else:
|
|
1264 multiplier = 1
|
|
1265
|
|
1266 try: return int(s) * multiplier
|
|
1267 except ValueError: return int(math.ceil(float(s) * multiplier))
|
|
1268
|
|
1269
|
|
1270 if __name__ == "__main__": main()
|
|
1271
|