]> git.donarmstrong.com Git - biopieces.git/blob - code_ruby/lib/maasha/align/pair.rb
added math_aux.rb
[biopieces.git] / code_ruby / lib / maasha / align / pair.rb
1 # Copyright (C) 2007-2012 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 # Module with stuff to create a pairwise aligment.
26 module PairAlign
27   # Class for creating a pairwise alignment.
28   class AlignPair
29     # Class method to create a pairwise alignment of two given Seq objects.
30     def self.align(q_entry, s_entry)
31       self.new(q_entry, s_entry)
32     end
33
34     # Method to inialize a pairwise alignment given two Seq objects.
35     def initialize(q_entry, s_entry)
36       @q_entry = q_entry
37       @s_entry = s_entry
38       @matches = []
39
40       @q_entry.seq.downcase!
41       @s_entry.seq.downcase!
42
43       align_recurse(@q_entry.seq, @s_entry.seq, 0, 0, @q_entry.length - 1, @s_entry.length - 1, 16, [])
44       matches_upcase
45       gaps_insert
46     end
47
48     private
49
50     # Method that creates an alignment by chaining matches, which are
51     # subsequences shared between two sequences. This recursive method
52     # functions by considering only matches within a given search space. If no
53     # matches are given these will be located and matches will be included
54     # depending on a calculated score. New search spaces spanning the spaces
55     # between the best scoring matches and the search space boundaries will be
56     # cast and recursed into.
57     def align_recurse(q_seq, s_seq, q_min, s_min, q_max, s_max, kmer, matches)
58       matches = matches_select_by_space(matches, q_min, s_min, q_max, s_max)
59
60       while (matches.size == 0 and kmer > 0)
61         matches = matches_find(q_seq, s_seq, q_min, s_min, q_max, s_max, kmer)
62         kmer /= 2
63       end
64
65       matches_score(matches, q_min, s_min, q_max, s_max)
66
67 #      matches.each { |m| puts m.to_s(q_seq) }
68
69       unless @matches.empty?
70         matches = matches.select { |match| match.score > 0 }
71       end
72
73       if best_match = matches.pop
74         @matches << best_match
75
76         l_q_min = q_min
77         l_s_min = s_min
78         l_q_max = best_match.q_beg - 1
79         l_s_max = best_match.s_beg - 1
80
81         r_q_min = best_match.q_end + 1
82         r_s_min = best_match.s_end + 1
83         r_q_max = q_max
84         r_s_max = s_max
85
86         if l_q_max - l_q_min > 0 and l_s_max - l_s_min > 0
87           align_recurse(q_seq, s_seq, l_q_min, l_s_min, l_q_max, l_s_max, kmer, matches)
88         end
89
90         if r_q_max - r_q_min > 0 and r_s_max - r_s_min > 0
91           align_recurse(q_seq, s_seq, r_q_min, r_s_min, r_q_max, r_s_max, kmer, matches)
92         end
93       end
94     end
95
96     # Method to select matches that lies within the search space.
97     def matches_select_by_space(matches, q_min, s_min, q_max, s_max)
98       new_matches = matches.select do |match|
99         match.q_beg >= q_min and
100         match.s_beg >= s_min and
101         match.q_end <= q_max and
102         match.s_end <= s_max
103       end
104
105       new_matches
106     end
107
108     def matches_score(matches, q_min, s_min, q_max, s_max)
109       matches.each do |match|
110         score_length = match_score_length(match)
111         score_diag   = match_score_diag(match, q_min, s_min, q_max, s_max)
112         score_corner = match_score_corner(match, q_min, s_min, q_max, s_max)
113
114
115         match.score = score_length - score_diag - score_corner
116         puts match
117         puts "score_length: #{score_length}   score_diag: #{score_diag}   score_corner: #{score_corner}  score: #{match.score}"
118       end
119
120       matches.sort_by! { |match| match.score }
121     end
122
123     def match_score_length(match)
124       match.length
125     end
126
127     def match_score_diag(match, q_min, s_min, q_max, s_max)
128         dist1 = (match.q_beg - match.s_beg).abs
129         dist2 = ((match.q_end - match.q_end).abs - dist1).abs
130
131         dist1 < dist2 ? dist1 : dist2
132     end
133
134     def match_score_corner(match, q_min, s_min, q_max, s_max)
135       1
136     end
137
138
139     # Method that finds all maximally expanded non-redundant matches shared
140     # between two sequences inside a given search space.
141     def matches_find(q_seq, s_seq, q_min, s_min, q_max, s_max, kmer)
142       matches   = []
143       redundant = Hash.new { |h, k| h[k] = [] }
144
145       s_index = index_seq(s_seq, s_min, s_max, kmer)
146
147       q_pos = q_min
148
149       while q_pos <= q_max - kmer + 1
150         q_oligo = q_seq[q_pos ... q_pos + kmer]
151
152         s_index[q_oligo].each do |s_pos|
153           match = Match.new(q_pos, s_pos, kmer)
154
155           unless match_redundant?(redundant, match)
156             match_expand(match, q_seq, s_seq, q_min, s_min, q_max, s_max)
157             matches << match
158
159             match_redundant_add(redundant, match)
160           end
161         end
162
163         q_pos += 1
164       end
165
166       matches
167     end
168
169     # Method that indexes a sequence within a given interval such that the
170     # index contains all oligos of a given kmer size and the positions where
171     # this oligo was located.
172     def index_seq(seq, min, max, kmer)
173       index_hash = Hash.new { |h, k| h[k] = [] }
174
175       pos = min
176
177       while pos <= max - kmer + 1
178         oligo = seq[pos ... pos + kmer]
179         index_hash[oligo] << pos
180
181         pos += 1
182       end
183
184       index_hash
185     end
186
187     # Method to check if a match is redundant.
188     def match_redundant?(redundant, match)
189       redundant[match.q_beg].each do |s_interval|
190         if s_interval.include? match.s_beg and s_interval.include? match.s_end
191           return true
192         end
193       end
194
195       false
196     end
197
198     # Method that adds a match to the redundancy index.
199     def match_redundant_add(redundant, match)
200       (match.q_beg .. match.q_end).each do |q|
201         redundant[q] << (match.s_beg .. match.s_end)
202       end
203     end
204
205     # Method that expands a match as far as possible to the left and right.
206     def match_expand(match, q_seq, s_seq, q_min, s_min, q_max, s_max)
207       match_expand_left(match, q_seq, s_seq, q_min, s_min)
208       match_expand_right(match, q_seq, s_seq, q_max, s_max)
209
210       match
211     end
212
213     # Method that expands a match as far as possible to the left.
214     def match_expand_left(match, q_seq, s_seq, q_min, s_min)
215       while match.q_beg > q_min and
216             match.s_beg > s_min and 
217             q_seq[match.q_beg - 1] == s_seq[match.s_beg - 1]
218         match.q_beg  -= 1
219         match.s_beg  -= 1
220         match.length += 1
221       end
222
223       match
224     end
225
226     # Method that expands a match as far as possible to the right.
227     def match_expand_right(match, q_seq, s_seq, q_max, s_max)
228       while match.q_end < q_max and
229             match.s_end < s_max and
230             q_seq[match.q_end + 1] == s_seq[match.s_end + 1]
231         match.length += 1
232       end
233
234       match
235     end
236
237     # Method for debugging purposes that upcase matching sequence while non-matches
238     # sequence is kept in lower case.
239     def matches_upcase
240       @matches.each do |match|
241         @q_entry.seq[match.q_beg .. match.q_end] = @q_entry.seq[match.q_beg .. match.q_end].upcase
242         @s_entry.seq[match.s_beg .. match.s_end] = @s_entry.seq[match.s_beg .. match.s_end].upcase
243       end
244     end
245
246     # Method that insert gaps in sequences based on a list of matches and thus
247     # creating an alignment.
248     def gaps_insert
249       @matches.sort_by! { |m| m.q_beg }
250
251       q_gaps = 0
252       s_gaps = 0
253
254       match = @matches.first
255       diff  = (q_gaps + match.q_beg) - (s_gaps + match.s_beg)
256
257       if diff < 0
258         @q_entry.seq.insert(0, "-" * diff.abs)
259         q_gaps += diff.abs
260       elsif diff > 0
261         @s_entry.seq.insert(0, "-" * diff.abs)
262         s_gaps += diff.abs
263       end
264
265       @matches[1 .. -1].each do |match|
266         diff = (q_gaps + match.q_beg) - (s_gaps + match.s_beg)
267
268         if diff < 0
269           @q_entry.seq.insert(match.q_beg + q_gaps, "-" * diff.abs)
270           q_gaps += diff.abs
271         elsif diff > 0
272           @s_entry.seq.insert(match.s_beg + s_gaps, "-" * diff.abs)
273           s_gaps += diff.abs
274         end
275       end
276
277       diff = @q_entry.length - @s_entry.length
278
279       if diff < 0
280         @q_entry.seq << ("-" * diff.abs)
281       else
282         @s_entry.seq << ("-" * diff.abs)
283       end
284     end
285   end
286
287   # Class for containing a match between two sequences q and s.
288   class Match
289     attr_accessor :q_beg, :s_beg, :length, :score
290
291     def initialize(q_beg, s_beg, length, score = 0.0)
292       @q_beg  = q_beg
293       @s_beg  = s_beg
294       @length = length
295       @score  = score
296     end
297
298     def q_end
299       @q_beg + @length - 1
300     end
301
302     def s_end
303       @s_beg + @length - 1
304     end
305
306     def to_s(seq = nil)
307       s = "q: #{@q_beg} #{q_end} s: #{@s_beg} #{s_end} l: #{@length} s: #{@score}"
308       s << " seq: #{seq[@q_beg .. q_end]}" if seq
309       s
310     end
311   end
312 end
313