in progress
[jalview.git] / forester / ruby / evoruby / lib / evo / io / parser / hmmscan_domain_extractor.rb
1 #
2 # = lib/evo/io/parser/hmmscan_domain_extractor.rb - HmmscanDomainExtractor class
3 #
4 # Copyright::  Copyright (C) 2012 Christian M. Zmasek
5 # License::    GNU Lesser General Public License (LGPL)
6 #
7 # $Id:  $
8
9
10 require 'lib/evo/util/constants'
11 require 'lib/evo/msa/msa_factory'
12 require 'lib/evo/io/msa_io'
13 require 'lib/evo/io/writer/fasta_writer'
14 require 'lib/evo/io/parser/fasta_parser'
15
16
17 module Evoruby
18
19   class HmmscanDomainExtractor
20
21     def initialize
22     end
23
24     # raises ArgumentError, IOError, StandardError
25     def parse( domain_id,
26         hmmscan_output,
27         fasta_sequence_file,
28         outfile,
29         passed_seqs_outfile,
30         failed_seqs_outfile,
31         e_value_threshold,
32         length_threshold,
33         add_position,
34         add_domain_number,
35         add_species,
36         min_linker,
37         log )
38
39       Util.check_file_for_readability( hmmscan_output )
40       Util.check_file_for_readability( fasta_sequence_file )
41       Util.check_file_for_writability( outfile + ".fasta" )
42       Util.check_file_for_writability( passed_seqs_outfile )
43       Util.check_file_for_writability( failed_seqs_outfile )
44
45       in_msa = nil
46       factory = MsaFactory.new()
47       in_msa = factory.create_msa_from_file( fasta_sequence_file, FastaParser.new() )
48
49       if ( in_msa == nil || in_msa.get_number_of_seqs() < 1 )
50         error_msg = "could not find fasta sequences in " + fasta_sequence_file
51         raise IOError, error_msg
52       end
53
54       out_msa = Msa.new
55
56       failed_seqs = Msa.new
57       passed_seqs = Msa.new
58       out_msa_pairs = nil
59       out_msa_isolated = nil
60       out_msa_singles = nil
61       out_msa_single_domains_protein_seqs = nil
62       out_msa_close_pairs_protein_seqs = nil
63       out_msa_close_pairs_only_protein_seqs = nil
64       out_msa_isolated_protein_seqs = nil
65       out_msa_isolated_only_protein_seqs = nil
66       out_msa_isolated_and_close_pair_protein_seqs = nil
67       if min_linker
68         out_msa_pairs = Msa.new
69         out_msa_isolated = Msa.new
70         out_msa_singles = Msa.new
71         out_msa_single_domains_protein_seqs = Msa.new
72         out_msa_close_pairs_protein_seqs = Msa.new
73         out_msa_close_pairs_only_protein_seqs = Msa.new
74         out_msa_isolated_protein_seqs = Msa.new
75         out_msa_isolated_only_protein_seqs = Msa.new
76         out_msa_isolated_and_close_pair_protein_seqs  = Msa.new
77       end
78
79       ld = Constants::LINE_DELIMITER
80
81       domain_pass_counter     = 0
82       domain_fail_counter     = 0
83       proteins_with_failing_domains = 0
84       max_domain_copy_number_per_protein = -1
85       max_domain_copy_number_sequence    = ""
86
87       hmmscan_datas = Array.new
88
89       File.open( hmmscan_output ) do | file |
90         while line = file.gets
91           if !is_ignorable?( line ) && line =~ /^\S+\s+/
92
93             #         tn      acc     tlen    query   acc     qlen    Evalue  score   bias    #       of      c-E     i-E     score   bias    hf      ht      af      at      ef      et      acc     desc
94             #         1       2       3       4       5       6       7       8       9       10      11      12      13      14      15      16      17      18      19      20      21      22      23
95             line =~ /^(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)/
96
97             if domain_id != $1
98               next
99             end
100
101             sequence = $4
102             number   = $10.to_i
103             out_of   = $11.to_i
104             env_from = $20.to_i
105             env_to   = $21.to_i
106             i_e_value  = $13.to_f
107
108             if ( ( ( e_value_threshold < 0.0 ) || ( i_e_value <= e_value_threshold ) ) &&
109                  ( ( length_threshold <= 0 )   || ( env_to - env_from + 1 ) >= length_threshold.to_f ) )
110               hmmscan_datas << HmmsearchData.new( sequence, number, out_of, env_from, env_to, i_e_value )
111               if ( number > max_domain_copy_number_per_protein )
112                 max_domain_copy_number_sequence    = sequence
113                 max_domain_copy_number_per_protein = number
114               end
115             else # failed
116               print( domain_fail_counter.to_s + ": " + sequence.to_s + " did not meet threshold(s)" )
117               log << domain_fail_counter.to_s + ": " + sequence.to_s + " did not meet threshold(s)"
118               if ( ( e_value_threshold.to_f >= 0.0 ) && ( i_e_value > e_value_threshold ) )
119                 print( " iE=" + i_e_value.to_s )
120                 log << " iE=" + i_e_value.to_s
121               end
122               if ( ( length_threshold.to_f > 0 ) && ( env_to - env_from + 1 ) < length_threshold.to_f )
123                 le = env_to - env_from + 1
124                 print( " l=" + le.to_s )
125                 log << " l=" + le.to_s
126               end
127               print( ld )
128               log << ld
129               domain_fail_counter  += 1
130             end
131
132             if number > out_of
133               error_msg = "number > out_of ! (this should not have happened)"
134               raise StandardError, error_msg
135             end
136
137             if number == out_of
138               if !hmmscan_datas.empty?
139                 process_hmmscan_datas( hmmscan_datas,
140                   in_msa,
141                   add_position,
142                   add_domain_number,
143                   add_species,
144                   out_msa,
145                   out_msa_singles,
146                   out_msa_pairs,
147                   out_msa_isolated,
148                   min_linker,
149                   out_msa_single_domains_protein_seqs,
150                   out_msa_close_pairs_protein_seqs,
151                   out_msa_close_pairs_only_protein_seqs,
152                   out_msa_isolated_protein_seqs,
153                   out_msa_isolated_only_protein_seqs,
154                   out_msa_isolated_and_close_pair_protein_seqs )
155                 domain_pass_counter += hmmscan_datas.length
156                 if passed_seqs.find_by_name_start( sequence, true ).length < 1
157                   add_sequence( sequence, in_msa, passed_seqs )
158                 else
159                   error_msg = "this should not have happened"
160                   raise StandardError, error_msg
161                 end
162               else
163                 if failed_seqs.find_by_name_start( sequence, true ).length < 1
164                   add_sequence( sequence, in_msa, failed_seqs )
165                   proteins_with_failing_domains += 1
166                 else
167                   error_msg = "this should not have happened"
168                   raise StandardError, error_msg
169                 end
170               end
171               hmmscan_datas.clear
172             end
173
174           end
175         end #  while line = file.gets
176       end # File.open( hmmsearch_output ) do | file |
177
178       if domain_pass_counter < 1
179         error_msg = "no domain sequences were extracted"
180         raise IOError, error_msg
181       end
182
183       log << ld
184       puts( "Max domain copy number per protein : " + max_domain_copy_number_per_protein.to_s )
185       log << "Max domain copy number per protein : " + max_domain_copy_number_per_protein.to_s
186       log << ld
187
188       if ( max_domain_copy_number_per_protein > 1 )
189         puts( "First protein with this copy number: " + max_domain_copy_number_sequence )
190         log << "First protein with this copy number: " + max_domain_copy_number_sequence
191         log << ld
192       end
193
194       write_msa( out_msa, outfile + ".fasta"  )
195       write_msa( passed_seqs, passed_seqs_outfile )
196       write_msa( failed_seqs, failed_seqs_outfile )
197
198       if out_msa_pairs
199         write_msa( out_msa_pairs, outfile +"_" + min_linker.to_s + ".fasta")
200       end
201
202       if out_msa_singles
203         write_msa( out_msa_singles, outfile +"_singles.fasta")
204       end
205
206       if out_msa_isolated
207         write_msa( out_msa_isolated, outfile + "_" + min_linker.to_s + "_isolated.fasta");
208       end
209
210       if out_msa_single_domains_protein_seqs
211         write_msa( out_msa_single_domains_protein_seqs, outfile +"_proteins_with_singles.fasta" )
212       end
213
214       if out_msa_close_pairs_protein_seqs
215         write_msa( out_msa_close_pairs_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_close_pairs.fasta" )
216       end
217
218       if out_msa_close_pairs_only_protein_seqs
219         write_msa( out_msa_close_pairs_only_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_close_pairs_only.fasta" )
220       end
221
222       if  out_msa_isolated_protein_seqs
223         write_msa(  out_msa_isolated_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_domains.fasta" )
224       end
225
226       if out_msa_isolated_only_protein_seqs
227         write_msa( out_msa_isolated_only_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_domains_only.fasta" )
228       end
229
230       if out_msa_isolated_and_close_pair_protein_seqs
231         write_msa( out_msa_isolated_and_close_pair_protein_seqs, outfile + "_" + min_linker.to_s + "_proteins_with_isolated_and_close_pairs.fasta" )
232       end
233
234       if min_linker
235         if ( out_msa_single_domains_protein_seqs.get_number_of_seqs +
236              out_msa_close_pairs_only_protein_seqs.get_number_of_seqs +
237              out_msa_isolated_only_protein_seqs.get_number_of_seqs +
238              out_msa_isolated_and_close_pair_protein_seqs.get_number_of_seqs ) != passed_seqs.get_number_of_seqs
239           error_msg = "this should not have happened"
240           raise StandardError, error_msg
241         end
242       end
243
244       log << ld
245       log << "passing domains                             : " + domain_pass_counter.to_s + ld
246       log << "failing domains                             : " + domain_fail_counter.to_s + ld
247       log << "input proteins                              : " + in_msa.get_number_of_seqs.to_s + ld
248       log << "proteins with passing domains               : " + passed_seqs.get_number_of_seqs.to_s + ld
249       log << "proteins with no passing domains            : " + proteins_with_failing_domains.to_s + ld
250       if min_linker
251         log << "min linker length                           : " + min_linker.to_s + ld
252         log << "single domains                              : " + out_msa_singles.get_number_of_seqs.to_s + ld
253         log << "domains in close pairs                      : " + (2 * out_msa_pairs.get_number_of_seqs).to_s + ld
254         log << "isolated domains                            : " + out_msa_isolated.get_number_of_seqs.to_s + ld
255         log << "proteins wih single domains                 : " + out_msa_single_domains_protein_seqs.get_number_of_seqs.to_s + ld
256         log << "proteins wih close pair domains             : " + out_msa_close_pairs_protein_seqs.get_number_of_seqs.to_s + ld
257         log << "proteins wih close pair domains only        : " + out_msa_close_pairs_only_protein_seqs.get_number_of_seqs.to_s + ld
258         log << "proteins wih isolated domains               : " + out_msa_isolated_protein_seqs.get_number_of_seqs.to_s + ld
259         log << "proteins wih isolated domains only          : " + out_msa_isolated_only_protein_seqs.get_number_of_seqs.to_s + ld
260         log << "proteins wih close pair and isolated domains: " + out_msa_isolated_and_close_pair_protein_seqs.get_number_of_seqs.to_s + ld
261       end
262
263       log << ld
264
265       return domain_pass_counter
266
267     end # parse
268
269
270     private
271
272     def write_msa( msa, filename )
273       io = MsaIO.new()
274       w = FastaWriter.new()
275       w.set_line_width( 60 )
276       w.clean( true )
277       begin
278         io.write_to_file( msa, filename, w )
279       rescue Exception
280         error_msg = "could not write to \"" + filename + "\""
281         raise IOError, error_msg
282       end
283     end
284
285
286     def add_sequence( sequence_name, in_msa, add_to_msa )
287       seqs = in_msa.find_by_name_start( sequence_name, true )
288       if ( seqs.length < 1 )
289         error_msg = "sequence \"" + sequence_name + "\" not found in sequence file"
290         raise StandardError, error_msg
291       end
292       if ( seqs.length > 1 )
293         error_msg = "sequence \"" + sequence_name + "\" not unique in sequence file"
294         raise StandardError, error_msg
295       end
296       seq = in_msa.get_sequence( seqs[ 0 ] )
297       add_to_msa.add_sequence( seq )
298     end
299
300     def process_hmmscan_datas( hmmscan_datas,
301         in_msa,
302         add_position,
303         add_domain_number,
304         add_species,
305         out_msa,
306         out_msa_singles,
307         out_msa_pairs,
308         out_msa_isolated,
309         min_linker,
310         out_msa_single_domains_protein_seqs,
311         out_msa_close_pairs_protein_seqs,
312         out_msa_close_pairs_only_protein_seqs,
313         out_msa_isolated_protein_seqs,
314         out_msa_isolated_only_protein_seqs,
315         out_msa_isolated_and_close_pair_protein_seqs )
316
317       actual_out_of = hmmscan_datas.size
318       saw_close_pair = false
319       saw_isolated = false
320
321       seq_name = ""
322       prev_seq_name = nil
323
324       hmmscan_datas.each_with_index do |hmmscan_data, index|
325         if hmmscan_data.number < ( index + 1 )
326           error_msg = "hmmscan_data.number < ( index + 1 ) (this should not have happened)"
327           raise StandardError, error_msg
328         end
329
330         seq_name =  hmmscan_data.seq_name
331
332         extract_domain( seq_name,
333           index + 1,
334           actual_out_of,
335           hmmscan_data.env_from,
336           hmmscan_data.env_to,
337           in_msa,
338           out_msa,
339           add_position,
340           add_domain_number,
341           add_species )
342
343         if min_linker
344           if actual_out_of == 1
345             extract_domain( seq_name,
346               1,
347               1,
348               hmmscan_data.env_from,
349               hmmscan_data.env_to,
350               in_msa,
351               out_msa_singles,
352               add_position,
353               add_domain_number,
354               add_species )
355             if out_msa_single_domains_protein_seqs.find_by_name_start( seq_name, true ).length < 1
356               add_sequence( seq_name, in_msa, out_msa_single_domains_protein_seqs )
357             else
358               error_msg = "this should not have happened"
359               raise StandardError, error_msg
360             end
361
362           else
363             first = index == 0
364             last = index == hmmscan_datas.length - 1
365
366             if ( ( first && ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker) )  ||
367                  ( last && ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) ||
368                  ( !first && !last &&  ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker ) &&
369                    ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) )
370
371               extract_domain( seq_name,
372                 index + 1,
373                 actual_out_of,
374                 hmmscan_data.env_from,
375                 hmmscan_data.env_to,
376                 in_msa,
377                 out_msa_isolated,
378                 add_position,
379                 add_domain_number,
380                 add_species )
381               saw_isolated = true
382
383             elsif !first
384               extract_domain( seq_name,
385                 index.to_s  + "+" + ( index + 1 ).to_s,
386                 actual_out_of,
387                 hmmscan_datas[ index - 1 ].env_from,
388                 hmmscan_data.env_to,
389                 in_msa,
390                 out_msa_pairs,
391                 add_position,
392                 add_domain_number,
393                 add_species )
394               saw_close_pair = true
395             end
396           end
397         end
398         if prev_seq_name && prev_seq_name != seq_name
399           error_msg = "this should not have happened"
400           raise StandardError, error_msg
401         end
402         prev_seq_name = seq_name
403       end # each
404       if saw_isolated
405         if out_msa_isolated_protein_seqs.find_by_name_start( seq_name, true ).length < 1
406           add_sequence( seq_name, in_msa, out_msa_isolated_protein_seqs )
407         else
408           error_msg = "this should not have happened"
409           raise StandardError, error_msg
410         end
411       end
412       if saw_close_pair
413         if out_msa_close_pairs_protein_seqs.find_by_name_start( seq_name, true ).length < 1
414           add_sequence( seq_name, in_msa, out_msa_close_pairs_protein_seqs )
415         else
416           error_msg = "this should not have happened"
417           raise StandardError, error_msg
418         end
419       end
420       if saw_close_pair && saw_isolated
421         if out_msa_isolated_and_close_pair_protein_seqs.find_by_name_start( seq_name, true ).length < 1
422           add_sequence( seq_name, in_msa, out_msa_isolated_and_close_pair_protein_seqs )
423         else
424           error_msg = "this should not have happened"
425           raise StandardError, error_msg
426         end
427       elsif saw_close_pair
428         if out_msa_close_pairs_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
429           add_sequence( seq_name, in_msa, out_msa_close_pairs_only_protein_seqs )
430         else
431           error_msg = "this should not have happened"
432           raise StandardError, error_msg
433         end
434       elsif saw_isolated
435         if out_msa_isolated_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
436           add_sequence( seq_name, in_msa, out_msa_isolated_only_protein_seqs )
437         else
438           error_msg = "this should not have happened"
439           raise StandardError, error_msg
440         end
441       end
442     end # def process_hmmscan_data
443
444     def extract_domain( sequence,
445         number,
446         out_of,
447         seq_from,
448         seq_to,
449         in_msa,
450         out_msa,
451         add_position,
452         add_domain_number,
453         add_species )
454       if  number.is_a?( Fixnum ) && ( number < 1 || out_of < 1 || number > out_of )
455         error_msg = "impossible: number=" + number.to_s + ", out of=" + out_of.to_s
456         raise StandardError, error_msg
457       end
458       if  seq_from < 1 || seq_to < 1 || seq_from >= seq_to
459         error_msg = "impossible: seq-f=" + seq_from.to_s + ", seq-t=" + seq_to.to_s
460         raise StandardError, error_msg
461       end
462       seqs = in_msa.find_by_name_start( sequence, true )
463       if seqs.length < 1
464         error_msg = "sequence \"" + sequence + "\" not found in sequence file"
465         raise IOError, error_msg
466       end
467       if seqs.length > 1
468         error_msg = "sequence \"" + sequence + "\" not unique in sequence file"
469         raise IOError, error_msg
470       end
471       # hmmsearch is 1 based, wheres sequences are 0 bases in this package.
472       seq = in_msa.get_sequence( seqs[ 0 ] ).get_subsequence( seq_from - 1, seq_to - 1 )
473
474       orig_name = seq.get_name
475
476       seq.set_name( orig_name.split[ 0 ] )
477
478       if add_position
479         seq.set_name( seq.get_name + "_" + seq_from.to_s + "-" + seq_to.to_s )
480       end
481
482       if out_of != 1 && add_domain_number
483         seq.set_name( seq.get_name + "~" + number.to_s + "-" + out_of.to_s )
484       end
485
486       if add_species
487         a = orig_name.rindex "["
488         b = orig_name.rindex "]"
489         unless a && b
490           error_msg = "species not found in " + orig_name
491           raise StandardError, error_msg
492         end
493         species = orig_name[ a .. b ]
494         seq.set_name( seq.get_name + " " + species )
495       end
496       out_msa.add_sequence( seq )
497     end
498
499     def is_ignorable?( line )
500       return ( line !~ /[A-Za-z0-9-]/ || line =~/^#/ )
501     end
502
503   end # class HmmscanDomainExtractor
504
505   class HmmsearchData
506     def initialize( seq_name, number, out_of, env_from, env_to, i_e_value )
507       @seq_name = seq_name
508       @number = number
509       @out_of = out_of
510       @env_from = env_from
511       @env_to = env_to
512       @i_e_value = i_e_value
513     end
514     attr_reader :seq_name, :number, :out_of, :env_from, :env_to, :i_e_value
515   end
516
517 end # module Evoruby
518