]> git.donarmstrong.com Git - biopieces.git/blob - code_ruby/lib/maasha/seq/backtrack.rb
fixing up backtrack code
[biopieces.git] / code_ruby / lib / maasha / seq / backtrack.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 'inline' # RubyInline
26
27 # Error class for all exceptions to do with BackTrack.
28 class BackTrackError < StandardError; end
29
30 # Module containing code to locate nucleotide patterns in sequences allowing for
31 # ambiguity codes and a given maximum mismatches, insertions, and deletions. The
32 # pattern match engine is based on a backtrack algorithm.
33 # Insertions are nucleotides found in the pattern but not in the sequence.
34 # Deletions are nucleotides found in the sequence but not in the pattern.
35 # Algorithm based on code kindly provided by j_random_hacker @ Stackoverflow:
36 # http://stackoverflow.com/questions/7557017/approximate-string-matching-using-backtracking/
37 module BackTrack
38   OK_PATTERN = Regexp.new('^[bflsycwphqrimtnkvadegu]+$')
39   MAX_MIS    = 5 # Maximum number of mismatches allowed
40   MAX_INS    = 5 # Maximum number of insertions allowed
41   MAX_DEL    = 5 # Maximum number of deletions allowed
42
43   # ------------------------------------------------------------------------------
44   #   str.scan(pattern[, max_mismatches[, max_insertions[, max_deletions]]])
45   #   -> Array
46   #   str.scan(pattern[, max_mismatches[, max_insertions[, max_deletions]]]) { |match|
47   #     block
48   #   }
49   #   -> Match
50   #
51   # ------------------------------------------------------------------------------
52   # Method to iterate through a sequence to locate pattern matches starting at a
53   # given offset allowing for a maximum number of mismatches, insertions, and
54   # deletions. Insertions are nucleotides found in the pattern but not in the
55   # sequence. Deletions are nucleotides found in the sequence but not in the
56   # pattern. Matches found in block context return the Match object. Otherwise
57   # matches are returned in an Array of Match objects.
58   def patscan(pattern, offset = 0, max_mismatches = 0, max_insertions = 0, max_deletions = 0)
59     raise BackTrackError, "Bad pattern: #{pattern}"                                          unless pattern.downcase =~ OK_PATTERN
60     raise BackTrackError, "offset: #{offset} out of range (0 ... #{self.length})"            unless (0 ... self.length).include? offset
61     raise BackTrackError, "max_mismatches: #{max_mismatches} out of range (0 .. #{MAX_MIS})" unless (0 .. MAX_MIS).include? max_mismatches
62     raise BackTrackError, "max_insertions: #{max_insertions} out of range (0 .. #{MAX_INS})" unless (0 .. MAX_INS).include? max_insertions
63     raise BackTrackError, "max_deletions: #{max_deletions} out of range (0 .. #{MAX_DEL})"   unless (0 .. MAX_DEL).include? max_deletions
64
65     matches = []
66
67     while result = track_C(self.seq, self.length, pattern, offset, max_mismatches, max_insertions, max_deletions)
68       pos, length, mis, ins, del = result
69
70       match = Match.new(pos, length, mis, ins, del, self.seq[pos ... pos + length])
71
72       if block_given?
73         yield match
74       else
75         matches << match
76       end
77
78       offset = match.pos + 1
79     end
80
81     return matches.empty? ? nil : matches unless block_given?
82   end
83
84   private
85
86   inline do |builder|
87     # Macro for matching nucleotides including ambiguity codes.
88     builder.prefix %{
89       #define MATCH(A,B) ((bitmap[A] & bitmap[B]) != 0)
90     }
91
92     # Defining a struct for returning match information.
93     builder.prefix %{
94       typedef struct 
95       {
96         unsigned int end;
97         unsigned int mis;
98         unsigned int ins;
99         unsigned int del;
100       } match;
101     }
102
103     # Bitmap for matching nucleotides including ambiguity codes.
104     # For each value bits are set from the left: bit pos 1 for A,
105     # bit pos 2 for T, bit pos 3 for C, and bit pos 4 for G.
106     builder.prefix %{
107       char bitmap[256] = {
108           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
109           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
110           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
111           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
112           0, 1,14, 4,11, 0, 0, 8, 7, 0, 0,10, 0, 5,15, 0,
113           0, 0, 9,12, 2, 2,13, 3, 0, 6, 0, 0, 0, 0, 0, 0,
114           0, 1,14, 4,11, 0, 0, 8, 7, 0, 0,10, 0, 5,15, 0,
115           0, 0, 9,12, 2, 2,13, 3, 0, 6, 0, 0, 0, 0, 0, 0,
116           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
117           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
118           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
119           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
120           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
121           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
122           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
123           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
124       };
125     }
126
127     # Backtrack algorithm for matching a pattern (p) starting in a sequence (s) allowing for mis
128     # mismatches, ins insertions and del deletions. ss is the start of the sequence, used only for
129     # reporting the match endpoints.
130     builder.prefix %{
131       match* backtrack(
132         char         *ss,   // Sequence start
133         char         *s,    // Sequence
134         char         *p,    // Pattern
135         unsigned int  mis,  // Max mismatches
136         unsigned int  ins,  // Max insertions
137         unsigned int  del   // Max deletions
138       )
139       {
140         match found = {0, 0, 0, 0};
141         match *f = &found;
142
143         while (*s && MATCH(*s, *p)) ++s, ++p;    // OK to always match longest segment
144
145         if (!*p)
146         {
147           f->end = (unsigned int) (s - ss);
148           f->mis = mis;
149           f->ins = ins;
150           f->del = del;
151
152           return f;
153         }
154         else
155         {
156           if (mis && *s && *p && (f = backtrack(ss, s + 1, p + 1, mis - 1, ins, del))) return f;
157           if (ins && *p &&       (f = backtrack(ss, s, p + 1, mis, ins - 1, del)))     return f;
158           if (del && *s &&       (f = backtrack(ss, s + 1, p, mis, ins, del - 1)))     return f;
159         }
160
161         return NULL;
162       }
163     }
164  
165     # Find pattern (p) in a sequence (s) starting at pos, with at most mis mismatches, ins
166     # insertions and del deletions.
167     builder.c %{
168       VALUE track_C(
169         VALUE _s,         // Sequence
170         VALUE _len,       // Sequence length
171         VALUE _p,         // Pattern
172         VALUE _pos,       // Start position
173         VALUE _max_mis,   // Maximum mismatches
174         VALUE _max_ins,   // Maximum insertions
175         VALUE _max_del    // Maximum deletions
176       )
177       {
178         char         *s       = StringValuePtr(_s);
179         char         *p       = StringValuePtr(_p);
180         unsigned int  len     = FIX2UINT(_len);
181         unsigned int  pos     = FIX2UINT(_pos);
182         unsigned int  max_mis = FIX2UINT(_max_mis);
183         unsigned int  max_ins = FIX2UINT(_max_ins);
184         unsigned int  max_del = FIX2UINT(_max_del);
185
186         char         *ss    = s;
187         match        *f     = NULL;
188         unsigned int  mbeg  = 0;
189         unsigned int  mlen  = 0;
190         unsigned int  mmis  = 0;
191         unsigned int  mins  = 0;
192         unsigned int  mdel  = 0;
193         VALUE         ary   = 0;
194
195         if (pos < len)
196         {
197           s += pos;
198
199           while (*s)
200           {
201             if ((f = backtrack(ss, s, p, max_mis, max_ins, max_del)))
202             {
203               mbeg = (s - ss);
204               mlen = f->end - mbeg;
205               mmis = max_mis - f->mis;
206               mins = max_ins - f->ins;
207               mdel = max_del - f->del;
208
209               ary = rb_ary_new();
210               rb_ary_push(ary, INT2FIX(mbeg));
211               rb_ary_push(ary, INT2FIX(mlen));
212               rb_ary_push(ary, INT2FIX(mmis));
213               rb_ary_push(ary, INT2FIX(mins));
214               rb_ary_push(ary, INT2FIX(mdel));
215               return ary;
216             }
217
218             s++;
219           }
220         }
221
222         return Qnil;
223       }
224     }
225   end
226
227   # Class containing match information.
228   class Match
229     attr_reader :pos, :length, :mis, :ins, :del, :match
230
231     def initialize(pos, length, mis, ins, del, match)
232       @pos    = pos
233       @length = length
234       @mis    = mis
235       @ins    = ins
236       @del    = del
237       @match  = match
238     end
239
240     def to_s
241       "#{@pos}:#{@length}:#{@mis}:#{@ins}:#{@del}:#{@match}"
242     end
243   end
244 end
245
246
247 __END__