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