]> git.donarmstrong.com Git - samtools.git/blob - misc/sam2vcf.pl
874ad5c19462b0e874b19cbdf6ab575271e53672
[samtools.git] / misc / sam2vcf.pl
1 #!/usr/bin/perl -w
2
3 # VCF specs: http://www.1000genomes.org/wiki/doku.php?id=1000_genomes:analysis:vcf3.3
4
5 # Contact: pd3@sanger
6 # Version: 2009-12-08
7
8 use strict;
9 use warnings;
10 use Carp;
11
12 my $opts = parse_params();
13 do_pileup_to_vcf($opts);
14
15 exit;
16
17 #---------------
18
19 sub error
20 {
21     my (@msg) = @_;
22     if ( scalar @msg ) { croak(@msg); }
23     die
24         "Usage: sam2vcf.pl [OPTIONS] < in.pileup > out.vcf\n",
25         "Options:\n",
26         "   -i, --indels-only                Ignore SNPs.\n",
27         "   -r, --refseq <file.fa>           The reference sequence, required when indels are present.\n",
28         "   -s, --snps-only                  Ignore indels.\n",
29         "   -t, --column-title <string>      The column title.\n",
30         "   -h, -?, --help                   This help message.\n",
31         "\n";
32 }
33
34
35 sub parse_params
36 {
37     my %opts = ();
38
39     $opts{fh_in}  = *STDIN;
40     $opts{fh_out} = *STDOUT;
41
42     while (my $arg=shift(@ARGV))
43     {
44         if ( $arg eq '-r' || $arg eq '--refseq' ) { $opts{refseq}=shift(@ARGV); next; }
45         if ( $arg eq '-t' || $arg eq '--column-title' ) { $opts{title}=shift(@ARGV); next; }
46         if ( $arg eq '-s' || $arg eq '--snps-only' ) { $opts{snps_only}=1; next; }
47         if ( $arg eq '-i' || $arg eq '--indels-only' ) { $opts{indels_only}=1; next; }
48         if ( $arg eq '-?' || $arg eq '-h' || $arg eq '--help' ) { error(); }
49
50         error("Unknown parameter \"$arg\". Run -h for help.\n");
51     }
52     return \%opts;
53 }
54
55 sub iupac_to_gtype
56 {
57     my ($ref,$base) = @_;
58     my %iupac = (
59             'K' => ['G','T'],
60             'M' => ['A','C'],
61             'S' => ['C','G'],
62             'R' => ['A','G'],
63             'W' => ['A','T'],
64             'Y' => ['C','T'],
65             );
66     if ( !exists($iupac{$base}) ) 
67     { 
68         if ( $base ne 'A' && $base ne 'C' && $base ne 'G' && $base ne 'T' ) { error("FIXME: what is this [$base]?\n"); }
69         if ( $ref eq $base ) { return ('.','0/0'); }
70         return ($base,'1/1');
71     }
72     my $gt = $iupac{$base};
73     if ( $$gt[0] eq $ref  ) { return ($$gt[1],'0/1'); }
74     elsif ( $$gt[1] eq $ref ) { return ($$gt[0],'0/1'); }
75     return ("$$gt[0],$$gt[1]",'1/2');
76 }
77
78
79 sub parse_indel
80 {
81     my ($cons) = @_;
82     if ( $cons=~/^-/ ) 
83     { 
84         my $len = length($');
85         return "D$len"; 
86     }
87     elsif ( $cons=~/^\+/ ) { return "I$'"; }
88     elsif ( $cons eq '*' ) { return undef; }
89     error("FIXME: could not parse [$cons]\n");
90 }
91
92
93 # An example of the pileup format:
94 #   1       3000011 C       C       32      0       98      1       ^~,     A
95 #   1       3002155 *       +T/+T   53      119     52      5       +T      *       4       1       0
96 #   1       3003094 *       -TT/-TT 31      164     60      11      -TT     *       5       6       0
97 #   1       3073986 *       */-AAAAAAAAAAAAAA       3       3       45      9       *       -AAAAAAAAAAAAAA 7       2       0
98 #
99 sub do_pileup_to_vcf
100 {
101     my ($opts) = @_;
102
103     my $fh_in  = $$opts{fh_in};
104     my $fh_out = $$opts{fh_out};
105     my ($prev_chr,$prev_pos,$prev_ref);
106     my $refseq;
107     my $ignore_indels = $$opts{snps_only} ? 1 : 0;
108     my $ignore_snps   = $$opts{indels_only} ? 1 : 0;
109     my $title = exists($$opts{title}) ? $$opts{title} : 'data';
110
111     print $fh_out 
112         qq[##fileformat=VCFv3.3\n],
113         qq[##INFO=DP,1,Integer,"Total Depth"\n],
114         qq[##FORMAT=GT,1,String,"Genotype"\n],
115         qq[##FORMAT=GQ,1,Integer,"Genotype Quality"\n],
116         qq[##FORMAT=DP,1,Integer,"Read Depth"\n],
117         qq[#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\t$title\n]
118         ;
119
120     while (my $line=<$fh_in>)
121     {
122         chomp($line);
123         my (@items) = split(/\t/,$line);
124         if ( scalar @items<8 ) 
125         { 
126             error("\nToo few columns, does not look like output of 'samtools pileup -c': $line\n"); 
127         }
128         my ($chr,$pos,$ref,$cons,$cons_qual,$snp_qual,$rms_qual,$depth,$a1,$a2) = @items;
129
130         my ($alt,$gt);
131         if ( $ref eq '*' )
132         {
133             # An indel is involved.
134             if ( $ignore_indels )
135             { 
136                 $prev_ref = $ref;
137                 $prev_pos = $pos;
138                 $prev_chr = $chr;
139                 next; 
140             }
141
142             if (!defined $prev_chr || $chr ne $prev_chr || $pos ne $prev_pos) 
143             {
144                 if ( !$$opts{refseq} ) { error("Cannot do indels without the reference.\n"); }
145                 if ( !$refseq ) { $refseq = Fasta->new(file=>$$opts{refseq}); }
146                 $ref = $refseq->get_base($chr,$pos);
147             }
148             else { $ref = $prev_ref; }
149
150             # One of the alleles can be a reference and it can come in arbitrary order. In some
151             #   cases */* can be encountered. In such a case, look in the additional columns.
152             my ($al1,$al2) = split(m{/},$cons);
153             if ( $al1 eq $al2 && $al1 eq '*' ) { $al1=$a1; $al2=$a2; }
154             my $alt1 = parse_indel($al1);
155             my $alt2 = parse_indel($al2);
156             if ( !$alt1 && !$alt2 ) { error("FIXME: could not parse indel:\n", $line); }
157             if ( !$alt1 ) 
158             { 
159                 $alt=$alt2; 
160                 $gt='0/1'; 
161             }
162             elsif ( !$alt2 ) 
163             { 
164                 $alt=$alt1; 
165                 $gt='0/1'; 
166             }
167             elsif ( $alt1 eq $alt2 )
168             { 
169                 $alt="$alt1"; 
170                 $gt='1/1'; 
171             }
172             else
173             { 
174                 $alt="$alt1,$alt2"; 
175                 $gt='1/2'; 
176             }
177         }
178         else
179         {
180             if ( $ignore_snps ) 
181             { 
182                 $prev_ref = $ref;
183                 $prev_pos = $pos;
184                 $prev_chr = $chr;
185                 next; 
186             }
187
188             # SNP
189             ($alt,$gt) = iupac_to_gtype($ref,$cons);
190         }
191
192         print $fh_out "$chr\t$pos\t.\t$ref\t$alt\t$snp_qual\t0\tDP=$depth\tGT:GQ:DP\t$gt:$cons_qual:$depth\n";
193
194         $prev_ref = $ref;
195         $prev_pos = $pos;
196         $prev_chr = $chr;
197     }
198 }
199
200
201 #------------- Fasta --------------------
202 #
203 # Uses samtools to get a requested base from a fasta file. For efficiency, preloads
204 #   a chunk to memory. The size of the cached sequence can be controlled by the 'size'
205 #   parameter.
206 #
207 package Fasta;
208
209 use strict;
210 use warnings;
211 use Carp;
212
213 sub Fasta::new
214 {
215     my ($class,@args) = @_;
216     my $self = {@args};
217     bless $self, ref($class) || $class;
218     if ( !$$self{file} ) { $self->throw(qq[Missing the parameter "file"\n]); }
219     $$self{chr}  = undef;
220     $$self{from} = undef;
221     $$self{to}   = undef;
222     if ( !$$self{size} ) { $$self{size}=10_000_000; }
223     bless $self, ref($class) || $class;
224     return $self;
225 }
226
227 sub read_chunk
228 {
229     my ($self,$chr,$pos) = @_;
230     my $to = $pos + $$self{size};
231     my $cmd = "samtools faidx $$self{file} $chr:$pos-$to";
232     my @out = `$cmd`;
233     if ( $? ) { $self->throw("$cmd: $!"); }
234     my $line = shift(@out);
235     if ( !($line=~/^>$chr:(\d+)-(\d+)/) ) { $self->throw("Could not parse: $line"); }
236     $$self{chr}  = $chr;
237     $$self{from} = $1;
238     $$self{to}   = $2;
239     my $chunk = '';
240     while ($line=shift(@out))
241     {
242         chomp($line);
243         $chunk .= $line;
244     }
245     $$self{chunk} = $chunk;
246     return;
247 }
248
249 sub get_base
250 {
251     my ($self,$chr,$pos) = @_;
252     if ( !$$self{chr} || $chr ne $$self{chr} || $pos<$$self{from} || $pos>$$self{to} )
253     {
254         $self->read_chunk($chr,$pos);
255     }
256     my $idx = $pos - $$self{from};
257     return substr($$self{chunk},$idx,1);
258 }
259
260 sub throw
261 {
262     my ($self,@msg) = @_;
263     croak(@msg);
264 }