2
$\begingroup$

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?

$\endgroup$
  • 1
    $\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 Sudbery Nov 15 '17 at 11:13
3
$\begingroup$

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 bufferof reads having the same name. If any read starts with a pattern, print the whole buffer. 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 - 
| improve this answer | |
$\endgroup$
  • $\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$ – Ian Sudbery Nov 15 '17 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$ – Pierre Nov 15 '17 at 11:17
  • $\begingroup$ I got it to work now with samjdk as explained in 3rd solution. Thanks a lot! $\endgroup$ – 719016 Nov 15 '17 at 16:38
3
$\begingroup$

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:

  1. Print the sequence at the beginning of the line and read the name in the second column
  2. Find all lines starting with the sequence
  3. 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:

  1. You might want to consider also reverse complementary sequences.
  2. You also might want to keep header of BAM file (you can extract it at the beginning and add it in the end).
| improve this answer | |
$\endgroup$
0
$\begingroup$

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)
| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.