]> git.donarmstrong.com Git - biopieces.git/blob - code_ruby/lib/maasha/align.rb
fixed SCORE_BASE global var
[biopieces.git] / code_ruby / lib / maasha / align.rb
1 # Copyright (C) 2007-2011 Martin A. Hansen.
2
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
17 # http://www.gnu.org/copyleft/gpl.html
18
19 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
20
21 # This software is part of the Biopieces framework (www.biopieces.org).
22
23 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
24
25 require 'pp'
26 require 'open3'
27 require 'narray'
28 require 'maasha/align/pair'
29 require 'maasha/fasta'
30
31 class AlignError < StandardError; end;
32
33 ALPH_DNA  = %w{A T C G}
34 ALPH_AMBI = %w{A T C G M R W S Y K V H D B N}
35
36 BIT_INDEL = 0
37 BIT_A = 1 << 0
38 BIT_T = 1 << 1
39 BIT_C = 1 << 2
40 BIT_G = 1 << 3
41
42 BIT_M = BIT_A | BIT_C
43 BIT_R = BIT_A | BIT_G
44 BIT_W = BIT_A | BIT_T
45 BIT_S = BIT_C | BIT_G
46 BIT_Y = BIT_C | BIT_T
47 BIT_K = BIT_G | BIT_T
48 BIT_V = BIT_A | BIT_C | BIT_G
49 BIT_H = BIT_A | BIT_C | BIT_T
50 BIT_D = BIT_A | BIT_G | BIT_T
51 BIT_B = BIT_C | BIT_G | BIT_T
52 BIT_N = BIT_G | BIT_A | BIT_T | BIT_C
53
54 BITMAP = [
55   BIT_A,
56   BIT_T,
57   BIT_C,
58   BIT_G,
59   BIT_M,
60   BIT_R,
61   BIT_W,
62   BIT_S,
63   BIT_Y,
64   BIT_K,
65   BIT_V,
66   BIT_H,
67   BIT_D,
68   BIT_B,
69   BIT_N
70 ]
71
72 TR_NUC = "-"                   + ALPH_AMBI.join("").downcase
73 TR_HEX = [BIT_INDEL].pack("C") + BITMAP.pack("C*")
74
75 ROW_A = 0
76 ROW_T = 1
77 ROW_C = 2
78 ROW_G = 3
79
80 class Fasta
81   def initialize(io)
82     @io = io
83   end
84
85   def close
86     @io.close
87   end
88 end
89
90 # Class for aligning sequences.
91 class Align
92   attr_accessor :options
93   attr_reader   :entries
94
95   include PairAlign
96
97   # Class method to align sequences in a list of Seq objects and
98   # return these as a new list of Seq objects.
99   def self.muscle(entries, verbose = false)
100     result   = []
101     index    = {}
102
103     Open3.popen3("muscle", "-quiet") do |stdin, stdout, stderr|
104       entries.each do |entry|
105         raise AlignError, "Duplicate sequence name: #{entry.seq_name}" if index.has_key? entry.seq_name
106
107         index[entry.seq_name] = entry.dup
108
109         stdin.puts entry.to_fasta
110       end
111
112       stdin.close
113
114       stderr.each_line { |line| $stderr.puts line } if verbose
115
116       aligned_entries = Fasta.new(stdout)
117
118       aligned_entries.each do |fa_entry|
119         fq_entry = index[fa_entry.seq_name]
120
121         fa_entry.seq.scan(/-+/) do |m|
122           fq_entry.seq  = fq_entry.seq[0 ... $`.length]  + ('-' * m.length) + fq_entry.seq[$`.length .. -1]
123           fq_entry.qual = fq_entry.qual[0 ... $`.length] + ('@' * m.length) + fq_entry.qual[$`.length .. -1] unless fq_entry.qual.nil?
124         end
125
126         result << fq_entry
127       end
128     end
129
130     self.new(result)
131   end
132   
133   # Class method to create a pairwise alignment of two given Seq objects. The
134   # alignment is created by casting a search space the size of the sequences
135   # and save the best scoring match between the sequences and recurse into
136   # the left and right search spaced around this match. When all search spaces
137   # are exhausted the saved matches are used to insert gaps in the sequences.
138   def self.pair(q_entry, s_entry)
139     AlignPair.align(q_entry, s_entry)
140
141     self.new([q_entry, s_entry])
142   end
143
144   # Method to initialize an Align object with a list of aligned Seq objects.
145   def initialize(entries, options = {})
146     @entries = entries
147     @options = options
148   end
149
150   # Method that returns the length of the alignment.
151   def length
152     @entries.first.length
153   end
154
155   # Method that returns the number of members or sequences in the alignment.
156   def members
157     @entries.size
158   end
159
160   # Method that returns the identity of an alignment with two members.
161   def identity
162     if self.members != 2
163       raise AlignError "Bad number of members for similarity calculation: #{self.members}"
164     end
165
166     na1 = NArray.to_na(@entries[0].seq.upcase, "byte")
167     na2 = NArray.to_na(@entries[1].seq.upcase, "byte")
168
169     shared   = (na1 - na2).count_false
170     total    = (@entries[0].length < @entries[1].length) ? @entries[0].length : @entries[1].length
171     identity = shared.to_f / total
172
173     identity
174   end
175
176   # Method to create a consensus sequence from an Align object and
177   # return a new Seq object with the consensus.
178   def consensus
179     cons = Consensus.new(@entries, @options)
180     cons.consensus
181   end
182
183   # Method to pretty print an alignment from an Align object.
184   def to_s
185     cons = Consensus.new(@entries, @options)
186     cons.mask_entries!
187
188     max_name = @entries.group_by { |entry| entry.seq_name.length }.max.first
189
190     output = ""
191
192     @entries.each do |entry|
193       output << entry.seq_name + (" " * (max_name + 3 - entry.seq_name.length )) + entry.seq + $/
194     end
195
196     cons_entry = cons.consensus 
197
198     output << " " * (max_name + 3) + cons_entry.seq
199     output << $/ + " " * (max_name + 3) + cons_entry.qual.tr("[@-h]", "           ..........ooooooooooOOOOOOOOOO") unless cons_entry.qual.nil?
200     output
201   end
202
203   # Method for iterating each of the aligned sequences.
204   def each
205     if block_given?
206       @entries.each { |entry| yield entry }
207     else
208       return @entries
209     end
210   end
211
212   private
213
214   class Consensus
215     # Method to initialize a Consensus object given a list of aligned Seq object.
216     def initialize(entries, options)
217       @entries = entries
218       @options = options
219
220       @cols = entries.first.seq.length
221       @rows = entries.size
222
223       @has_qual = entries.first.qual.nil? ? false : true
224
225       @na_seq  = NArray.byte(@cols, @rows)
226       @na_qual = NArray.byte(@cols, @rows) if @has_qual
227
228       na_add_entries
229       consensus_calc
230     end
231
232     # Method that lowercase residues that have been removed during
233     # the determination of the consensus sequence.
234     def mask_entries!
235       na_seq = NArray.byte(@cols, @rows)
236
237       @entries.each_with_index do |entry, i|
238         na_seq[true, i]  = NArray.to_na(entry.seq.upcase, "byte")
239       end
240
241       na_seq += ((na_seq.ne('-'.ord) - @na_seq.ne(0)) * ' '.ord)
242
243       @entries.each_with_index do |entry, i|
244         entry.seq = na_seq[true, i].to_s
245       end
246     end
247
248     # Method that returns a Sequence object with a consensus sequence
249     # for the entries in an Align object.
250     def consensus
251       new_seq      = Seq.new
252       new_seq.seq  = consensus_seq
253       new_seq.qual = consensus_qual if @has_qual
254       new_seq.type = "dna"
255
256       new_seq
257     end
258
259     private
260
261     # Method to add a Seq entry object to the two NArrays; @na_seq and @na_qual
262     def na_add_entries
263       @entries.each_with_index do |entry, i|
264         @na_seq[true, i]  = NArray.to_na(entry.seq.downcase.tr(TR_NUC, TR_HEX), "byte")
265         @na_qual[true, i] = NArray.to_na(entry.qual, "byte") - Seq::SCORE_BASE if @has_qual
266       end
267     end
268
269     # Method to calculate a consensus sequence from a list of sequenced stored in two
270     # NArrays.
271     def consensus_calc
272       if @has_qual
273         if @options.has_key? :quality_min
274           mask = mask_quality_min
275
276           @na_seq  *= mask
277           @na_qual *= mask
278         end
279
280         if @options.has_key? :quality_mean
281           mask = mask_quality_mean
282
283           @na_seq  *= mask
284           @na_qual *= mask
285         end
286       end
287
288       if @options.has_key? :sequence_min
289         mask = mask_sequence_min
290
291         @na_seq  *= mask
292         @na_qual *= mask if @has_qual
293       end
294
295       if @options.has_key? :gap_max
296         mask = mask_gap_max
297
298         @na_seq  *= mask
299         @na_qual *= mask if @has_qual
300       end
301
302       if @options.has_key? :residue_min
303         mask = mask_residue_min
304
305         @na_seq  *= mask
306         @na_qual *= mask if @has_qual
307       end
308     end
309
310     # Mask that indicates which columns have more than sequence_min sequences.
311     # Columns with less than sequence_min are 0'ed, otherwise set to 1.
312     def mask_sequence_min
313       mask  = NArray.byte(@cols, @rows) + 1
314       mask *= ((@na_seq > 0).to_type("int").sum(1) >= @options[:sequence_min])
315       mask
316     end
317
318     # Mask that indicates which residue frequencies that are above the residue_min.
319     # The residue frequencies are calculated for each column and residue type as the
320     # number of each residue type over the sum of all non-gap residues in that column.
321     # Positions with residues above the residue_min are indicated with 1.
322     def mask_residue_min
323       cons_min = @options[:residue_min]
324       factor   = 1 / @na_seq.ne(0).to_type("float").sum(1)
325
326       mask_A = (@na_seq & BIT_A > 0).to_type("int")
327       mask_T = (@na_seq & BIT_T > 0).to_type("int")
328       mask_C = (@na_seq & BIT_C > 0).to_type("int")
329       mask_G = (@na_seq & BIT_G > 0).to_type("int")
330
331       mask_A = (mask_A * mask_A.sum(1)) * factor >= cons_min
332       mask_T = (mask_T * mask_T.sum(1)) * factor >= cons_min
333       mask_C = (mask_C * mask_C.sum(1)) * factor >= cons_min
334       mask_G = (mask_G * mask_G.sum(1)) * factor >= cons_min
335
336       mask_A | mask_T | mask_C | mask_G
337     end
338
339     # Mask that indicates which columns contain fewer gaps than max_gap.
340     # Columns with more gaps are 0'ed, others are set to 1.
341     def mask_gap_max
342       mask  = NArray.byte(@cols, @rows) + 1
343       mask *= @na_seq.ne(0).to_type("float").sum(1) / @rows > @options[:gap_max]
344
345       mask
346     end
347
348     # Mask that indicates which residues in an alignment are above quality_min.
349     # Positions with subquality are 0'ed - all others are set to 1.
350     def mask_quality_min
351       @na_qual > @options[:quality_min]
352     end
353
354     # Mask that indicates which columns have a quality mean above quality_mean which
355     # is the mean of all non-gap quality residues in that column. Columns with less then
356     # quality_mean are 0'ed, otherwise set to 1.
357     def mask_quality_mean
358       mask     = NArray.byte(@cols, @rows) + 1
359       residues = @na_seq.ne(0).to_type("int").sum(1)
360       quality  = @na_qual.to_type("float").sum(1)
361
362       mask * (quality / residues).round > @options[:quality_mean]
363     end
364
365     # Method to calculate a consensus sequence from a Consensus object.
366     def consensus_seq
367       cons  = NArray.byte(@cols)
368       cons |= (@na_seq & BIT_A).max(1)
369       cons |= (@na_seq & BIT_T).max(1)
370       cons |= (@na_seq & BIT_C).max(1)
371       cons |= (@na_seq & BIT_G).max(1)
372
373       cons.to_s.tr!(TR_HEX, TR_NUC).upcase
374     end
375
376     # Method to calculate a consensus quality from a Consensus object.
377     def consensus_qual
378       (@na_qual.mean(1).round.to_type("byte") + Seq::SCORE_BASE).to_s
379     end
380   end
381 end
382
383 __END__