What's the most straightforward way to filter a BAM file for read pairs where one or both of the reads starts with a given sequence pattern? Would a tool that deals with UMIs work for this? Which one would be best?
-
2$\begingroup$ Are you interested in READs that start with a particular sequence or ALIGNMENTS that start with a particular sequence, because they are not the same thing for 2 reasons: 1) The sam format records sequences in genome orientation. Thus if the sequence of the read starts AAAA, but the read maps to the '-' strand the BAM seq field will not start AAAA, but end TTTT 2) Mappers may clip or otherwise only map part of a read. When they do this, they might only record part of the sequence of the read in the SEQ field. Thus a line with the CIGAR 5H90M will not contain the first 5nt of the read seq $\endgroup$– Ian SudberyNov 15, 2017 at 11:13
3 Answers
First option
My solution uses the following steps:
- use picard sortsam to sort the records on query-name (not samtools sort because the order is not the same between java and C )
- use jjs (java scripting engine) and the htsjdk library to build a
buffer
of reads having the same name. If any read starts with a pattern, print the wholebuffer
. The htsjdk should be already contained in the picard.jar. - sort back to coordinate order using samtools.
All in one:
java -jar /path/to/picard.jar SortSam I=in.bam O=/dev/stdout SO=queryname |\
jjs -cp /path/to/picard.jar script.js |\
samtools sort -T TMP -o out.bam -
with script.js
var out=java.lang.System.out;
var File = Java.type("java.io.File");
var SamReaderFactory = Java.type("htsjdk.samtools.SamReaderFactory");
var SamInputResource = Java.type("htsjdk.samtools.SamInputResource");
var SAMFileWriterFactory = Java.type("htsjdk.samtools.SAMFileWriterFactory");
var samReader = SamReaderFactory.makeDefault().open(SamInputResource.of(java.lang.System.in));
var samWriter = new SAMFileWriterFactory().makeBAMWriter(samReader.getFileHeader(),true,java.lang.System.out);
var iter=samReader.iterator();
var buffer=[];
for(;;)
{
var rec=null;
if(iter.hasNext()) rec=iter.next();
if(rec==null || (buffer.length>0 && !rec.getReadName().equals(buffer[0].getReadName())))
{
var i=0;
for(i=0;i< buffer.length;++i)
{
if(buffer[i].getReadString().match(/^AAAA/)) break;
}
if(i!=buffer.length)
{
for(i=0;i< buffer.length ;++i)
{
samWriter.addAlignment(buffer[i]);
}
}
if(rec==null) break;
buffer=[];
}
buffer.push(rec);
}
samReader.close();
samWriter.close();
A second option is to use samtools view and awk to export the read names:
samtools view input.bam | awk -F '\t' '{if($10 ~ /^AAAA/) print $1;}' | sort | uniq > names.txt
and to use Picard FilterSamReads and READ_LIST_FILE to filter the reads.
UPDATE: 3rd solution:
I've updated my tool samjdk: there is now a new option --pair
to work with query-name-sorted BAM files.
The command would be:
java -jar picard.jar SortSam I=S1.bam O=/dev/stdout SO=queryname |\
java -jar dist/samjdk.jar --samoutputformat BAM --pair \
-e 'return records.stream().anyMatch(R->R.getReadString().startsWith("AAAAAAG"));' |\
samtools sort -T TMP -o out.bam -
-
$\begingroup$ Does this account for the fact that BAM stores the sequence of a read in genome orientation, rather than read orientation? So if the sequence of a read mapped to the plus strand starts with AAAA it will be recorded in the BAM as ending with TTTT. $\endgroup$ Nov 15, 2017 at 11:08
-
$\begingroup$ @ia no but it would be easy to test this:
(R.getReadNegativeStrandFlag()?R.getReadString().endsWith("TTTT"):R.getReadString().startsWith("AAAA"))
$\endgroup$– PierreNov 15, 2017 at 11:17 -
$\begingroup$ I got it to work now with samjdk as explained in 3rd solution. Thanks a lot! $\endgroup$– 719016Nov 15, 2017 at 16:38
For at least one in a pair matching the sequence, if the read names of pairs are identical, some grep magic could do the job:
- Print the sequence at the beginning of the line and read the name in the second column
- Find all lines starting with the sequence
- Extract only read names
And then just grep all found reads and save into the BAM file:
SEQ=<my_sequence>
samtools view unfiltered.bam | \
awk '{print $10 "\t" $1}' | \
grep "^$SEQ" | \
cut -f 2 > list_of_matching_names
samtools view unfiltered.bam | \
grep -f list_of_matching_names | \
samtools view -b > filtered.bam
Two small notes:
- You might want to consider also reverse complementary sequences.
- You also might want to keep header of BAM file (you can extract it at the beginning and add it in the end).
I am one of the authors of the UMI-tools
pacakge, and although you probably could hack together a way to do this with our package, its not what its designed for and the solutions above are probably better.
The best way to do this would be to filter the fastq before mapping if thats a possibility, if it has to be the BAM, then here is a python implementation similar to Pierres java solution above. This one fiddles about with strands and cigar operations a bit more.
First sort the reads by name with samtools sort -n
then
import pysam
inbam = pysam.AlignmentFile("mybam.bam")
outbam = pysam.AlignmentFile("mybam.filtered.bam", "wb", template=inbam)
seq_to_find = "AAGG"
rc_seq_to_find = "CCTT"
def test_and_out_buffer(read_buffer, outbam, seq_to_find, rc_seq_to_find):
'''This function takes a list of reads and check if any of them
have the prespecific sequences at their start. If the sequence
is detected the whole buffer is output. The start of the read
is the end of the alignment if the read is on the '-' strand.
Also checks that the read doesn't start with hard-clipped sequence'''
do_output=False
for segment in read_buffer:
if segment.is_reverse:
# the last operation is stored in the first part of the
#last of the cigar tuples. Operation 5 is Hard-clip
output_condition = (not segment.cigartuples[-1][0] == 5) and \
segment.query_sequence.endswith(rc_seq_to_find)
else:
output_condition = (not segment.cigartuples[0][0] == 5) and \
segment.query_sequence.startswith(seq_to_find)
if output_condition:
do_output=True
break
if do_output:
for segment in read_buffer:
outbam.write(segment)
last_name=""
read_buffer=[]
# Loop over reads in the file
for read in inbam.fetch(until_eof=True):
# if the name hasn't changed, the name is part of the
# same "pair"
if read.query_name==last_name:
buffer.append(read)
continue
# if we got here then the a new pair is being examined
# test the old pair and output if neccesary.
test_and_output_buffer(read_buffer, outbam, seq_to_find, rc_seq_to_find)
# make a new buffer with just the single new read.
last_name=read.query_name
read_buffer=[read]
# the last set of reds will not have been output.
test_and_output_buffer(read_buffer, outbam, seq_to_find, rc_seq_to_find)