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