X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=code_ruby%2Flib%2Fmaasha%2Fseq.rb;h=4a0af47ed0f5fde9d5ffe87ca19a3caec087f6d6;hb=b982cc677363d7458963b610ec53bf2c4f7476a9;hp=55c9c1f31b9cf7b2adabd14e65436766474bf580;hpb=a761931fd09d82e1924dd0b3031fdcdf07f0d9a8;p=biopieces.git diff --git a/code_ruby/lib/maasha/seq.rb b/code_ruby/lib/maasha/seq.rb index 55c9c1f..4a0af47 100644 --- a/code_ruby/lib/maasha/seq.rb +++ b/code_ruby/lib/maasha/seq.rb @@ -23,31 +23,60 @@ # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< require 'maasha/bits' -require 'maasha/seq/backtrack' require 'maasha/seq/digest' -#require 'maasha/seq/patscan' -require 'maasha/seq/patternmatcher' require 'maasha/seq/trim' require 'narray' +autoload :BackTrack, 'maasha/seq/backtrack.rb' +autoload :Dynamic, 'maasha/seq/dynamic.rb' + # Residue alphabets DNA = %w[a t c g] RNA = %w[a u c g] PROTEIN = %w[f l s y c w p h q r i m t n k v a d e g] INDELS = %w[. - _ ~] -# Quality scores bases -SCORE_BASE = 64 -SCORE_MIN = 0 -SCORE_MAX = 40 +# Translation table 11 +# (http://www.ncbi.nlm.nih.gov/Taxonomy/taxonomyhome.html/index.cgi?chapter=cgencodes#SG11) +# AAs = FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG +# Starts = ---M---------------M------------MMMM---------------M------------ +# Base1 = TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG +# Base2 = TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG +# Base3 = TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG +TRANS_TAB11_START = { + "TTG" => "M", "CTG" => "M", "ATT" => "M", "ATC" => "M", + "ATA" => "M", "ATG" => "M", "GTG" => "M" +} + +TRANS_TAB11 = { + "TTT" => "F", "TCT" => "S", "TAT" => "Y", "TGT" => "C", + "TTC" => "F", "TCC" => "S", "TAC" => "Y", "TGC" => "C", + "TTA" => "L", "TCA" => "S", "TAA" => "*", "TGA" => "*", + "TTG" => "L", "TCG" => "S", "TAG" => "*", "TGG" => "W", + "CTT" => "L", "CCT" => "P", "CAT" => "H", "CGT" => "R", + "CTC" => "L", "CCC" => "P", "CAC" => "H", "CGC" => "R", + "CTA" => "L", "CCA" => "P", "CAA" => "Q", "CGA" => "R", + "CTG" => "L", "CCG" => "P", "CAG" => "Q", "CGG" => "R", + "ATT" => "I", "ACT" => "T", "AAT" => "N", "AGT" => "S", + "ATC" => "I", "ACC" => "T", "AAC" => "N", "AGC" => "S", + "ATA" => "I", "ACA" => "T", "AAA" => "K", "AGA" => "R", + "ATG" => "M", "ACG" => "T", "AAG" => "K", "AGG" => "R", + "GTT" => "V", "GCT" => "A", "GAT" => "D", "GGT" => "G", + "GTC" => "V", "GCC" => "A", "GAC" => "D", "GGC" => "G", + "GTA" => "V", "GCA" => "A", "GAA" => "E", "GGA" => "G", + "GTG" => "V", "GCG" => "A", "GAG" => "E", "GGG" => "G" +} + # Error class for all exceptions to do with Seq. class SeqError < StandardError; end class Seq - #include Patscan - include PatternMatcher - include BackTrack + # Quality scores bases + SCORE_BASE = 64 + SCORE_MIN = 0 + SCORE_MAX = 40 + include Digest include Trim @@ -121,6 +150,7 @@ class Seq # by inspecting the first 100 residues. def type_guess! self.type = self.type_guess + self end # Returns the length of a sequence. @@ -190,6 +220,56 @@ class Seq self.seq.tr!('Uu','Tt') end + # Method to translate a DNA sequence to protein. + def translate!(trans_tab = 11) + raise SeqError, "Sequence type must be 'dna' - not #{self.type}" unless self.type == 'dna' + raise SeqError, "Sequence length must be a multiplum of 3 - was: #{self.length}" unless (self.length % 3) == 0 + + case trans_tab + when 11 + codon_start_hash = TRANS_TAB11_START + codon_hash = TRANS_TAB11 + else + raise SeqError, "Unknown translation table: #{trans_tab}" + end + + codon = self.seq[0 ... 3].upcase + + aa = codon_start_hash[codon] + + raise SeqError, "Unknown start codon: #{codon}" if aa.nil? + + protein = aa + + i = 3 + + while i < self.length + codon = self.seq[i ... i + 3].upcase + + aa = codon_hash[codon] + + raise SeqError, "Unknown codon: #{codon}" if aa.nil? + + protein << aa + + i += 3 + end + + self.seq = protein + self.qual = nil + self.type = "protein" + + self + end + + alias :to_protein! :translate! + + def translate(trans_tab = 11) + self.dup.translate!(trans_tab) + end + + alias :to_protein :translate + # Method that given a Seq entry returns a Biopieces record (a hash). def to_bp raise SeqError, "Missing seq_name" if self.seq_name.nil? @@ -255,17 +335,13 @@ class Seq key end - # Method to reverse complement sequence. - def reverse_complement - self.reverse - self.complement - self + # Method to reverse the sequence. + def reverse + Seq.new(self.seq_name, self.seq.reverse, self.type, self.qual ? self.qual.reverse : self.qual) end - alias :revcomp :reverse_complement - # Method to reverse the sequence. - def reverse + def reverse! self.seq.reverse! self.qual.reverse! if self.qual self @@ -275,6 +351,26 @@ class Seq def complement raise SeqError, "Cannot complement 0 length sequence" if self.length == 0 + entry = Seq.new + entry.seq_name = self.seq_name + entry.type = self.type + entry.qual = self.qual + + if self.is_dna? + entry.seq = self.seq.tr('AGCUTRYWSMKHDVBNagcutrywsmkhdvbn', 'TCGAAYRWSKMDHBVNtcgaayrwskmdhbvn') + elsif self.is_rna? + entry.seq = self.seq.tr('AGCUTRYWSMKHDVBNagcutrywsmkhdvbn', 'UCGAAYRWSKMDHBVNucgaayrwskmdhbvn') + else + raise SeqError, "Cannot complement sequence type: #{self.type}" + end + + entry + end + + # Method that complements sequence including ambiguity codes. + def complement! + raise SeqError, "Cannot complement 0 length sequence" if self.length == 0 + if self.is_dna? self.seq.tr!('AGCUTRYWSMKHDVBNagcutrywsmkhdvbn', 'TCGAAYRWSKMDHBVNtcgaayrwskmdhbvn') elsif self.is_rna? @@ -282,6 +378,8 @@ class Seq else raise SeqError, "Cannot complement sequence type: #{self.type}" end + + self end # Method to determine the Hamming Distance between @@ -311,7 +409,12 @@ class Seq seq_new end - # Method to shuffle a sequence readomly inline. + # Method to return a new Seq object with shuffled sequence. + def shuffle + Seq.new(self.seq_name, self.seq.split('').shuffle!.join, self.type, self.qual) + end + + # Method to shuffle a sequence randomly inline. def shuffle! self.seq = self.seq.split('').shuffle!.join self @@ -324,7 +427,6 @@ class Seq raise SeqError, "subsequence length: #{length} < 0" if length < 0 raise SeqError, "subsequence start + length > Seq.length: #{start} + #{length} > #{self.length}" if start + length > self.length - if length == 0 seq = "" qual = "" unless self.qual.nil? @@ -335,26 +437,18 @@ class Seq qual = self.qual[start .. stop] unless self.qual.nil? end - - Seq.new(self.seq_name, seq, self.type, qual) + Seq.new(self.seq_name, seq, self.type, qual) # TODO changed self.seq_name.dup to self.seq_name -> consequence? end # Method that replaces a sequence with a subsequence from a given start position # and of a given length. def subseq!(start, length = self.length - start) - raise SeqError, "subsequence start: #{start} < 0" if start < 0 - raise SeqError, "subsequence length: #{length} < 0" if length < 0 - raise SeqError, "subsequence start + length > Seq.length: #{start} + #{length} > #{self.length}" if start + length > self.length - - if length == 0 - self.seq = "" - self.qual = "" unless self.qual.nil? - else - stop = start + length - 1 + s = subseq(start, length) - self.seq = self.seq[start .. stop] - self.qual = self.qual[start .. stop] unless self.qual.nil? - end + self.seq_name = s.seq_name + self.seq = s.seq + self.type = s.type + self.qual = s.qual self end @@ -413,21 +507,6 @@ class Seq ((self.seq.scan(/[a-z]/).size.to_f / (self.len - self.indels).to_f) * 100).round(2) end - # Hard masks sequence residues where the corresponding quality score - # is below a given cutoff. - def mask_seq_hard_old(cutoff) - seq = self.seq.upcase - scores = self.qual - i = 0 - - scores.each_char do |score| - seq[i] = 'N' if score.ord - SCORE_BASE < cutoff - i += 1 - end - - self.seq = seq - end - # Hard masks sequence residues where the corresponding quality score # is below a given cutoff. def mask_seq_hard!(cutoff) @@ -466,11 +545,41 @@ class Seq self end + # Method that determines if a quality score string can be + # absolutely identified as base 33. + def qual_base33? + self.qual.match(/[!-:]/) ? true : false + end + + # Method that determines if a quality score string can be + # absolutely identified as base 64. + def qual_base64? + self.qual.match(/[K-h]/) ? true : false + end + + # Method to determine if a quality score is valid. + def qual_valid?(encoding) + raise SeqError, "Missing qual" if self.qual.nil? + + case encoding.downcase + when "sanger" then return true if self.qual.match(/^[!-~]*$/) + when "454" then return true if self.qual.match(/^[@-~]*$/) + when "solexa" then return true if self.qual.match(/^[;-~]*$/) + when "illumina13" then return true if self.qual.match(/^[@-~]*$/) + when "illumina15" then return true if self.qual.match(/^[@-~]*$/) + when "illumina18" then return true if self.qual.match(/^[!-~]*$/) + else raise SeqError, "unknown quality score encoding: #{encoding}" + end + + false + end + # Method to convert quality scores inbetween formats. # Sanger base 33, range 0-40 + # 454 base 64, range 0-40 # Solexa base 64, range -5-40 # Illumina13 base 64, range 0-40 - # Illumina15 base 64, range 3-40 + # Illumina15 base 64, range 0-40 # Illumina18 base 33, range 0-41 def convert_scores!(from, to) unless from == to @@ -478,6 +587,7 @@ class Seq case from.downcase when "sanger" then na_qual -= 33 + when "454" then na_qual -= 64 when "solexa" then na_qual -= 64 when "illumina13" then na_qual -= 64 when "illumina15" then na_qual -= 64 @@ -487,11 +597,12 @@ class Seq case to.downcase when "sanger" then na_qual += 33 + when "454" then na_qual += 64 when "solexa" then na_qual += 64 when "illumina13" then na_qual += 64 when "illumina15" then na_qual += 64 when "illumina18" then na_qual += 33 - else raise SeqError, "unknown quality score encoding: #{from}" + else raise SeqError, "unknown quality score encoding: #{to}" end self.qual = na_qual.to_s @@ -508,6 +619,47 @@ class Seq na_qual -= SCORE_BASE na_qual.mean end + + # Method to find open reading frames (ORFs). + def each_orf(size_min, size_max, start_codons, stop_codons, pick_longest = false) + orfs = [] + pos_beg = 0 + + regex_start = Regexp.new(start_codons.join('|'), true) + regex_stop = Regexp.new(stop_codons.join('|'), true) + + while pos_beg and pos_beg < self.length - size_min + if pos_beg = self.seq.index(regex_start, pos_beg) + if pos_end = self.seq.index(regex_stop, pos_beg) + length = (pos_end - pos_beg) + 3 + + if (length % 3) == 0 + if size_min <= length and length <= size_max + subseq = self.subseq(pos_beg, length) + + orfs << [subseq, pos_beg, pos_end + 3] + end + end + end + + pos_beg += 1 + end + end + + if pick_longest + orf_hash = {} + + orfs.each { |orf| orf_hash[orf.last] = orf unless orf_hash[orf.last] } + + orfs = orf_hash.values + end + + if block_given? + orfs.each { |orf| yield orf } + else + return orfs + end + end end __END__