a2d0c3ff69b0acb62f09100c78377562408b6045
[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
235       log << ld
236       log << "passing domains                 : " + domain_pass_counter.to_s + ld
237       if ( min_linker )
238         log << "single domains                  : " + out_msa_singles.get_number_of_seqs.to_s + ld
239         log << "domains in close pairs          : " + (2 * out_msa_pairs.get_number_of_seqs).to_s + ld
240         log << "isolated domains                : " + out_msa_isolated.get_number_of_seqs.to_s + ld
241       end
242       log << "failing domains                 : " + domain_fail_counter.to_s + ld
243       log << "proteins with passing domains   : " + passed_seqs.get_number_of_seqs.to_s + ld
244       log << "proteins with no passing domains: " + proteins_with_failing_domains.to_s + ld
245       log << ld
246
247       return domain_pass_counter
248
249     end # parse
250
251
252     private
253
254     def write_msa( msa, filename )
255       io = MsaIO.new()
256       w = FastaWriter.new()
257       w.set_line_width( 60 )
258       w.clean( true )
259       begin
260         io.write_to_file( msa, filename, w )
261       rescue Exception
262         error_msg = "could not write to \"" + filename + "\""
263         raise IOError, error_msg
264       end
265     end
266
267
268     def add_sequence( sequence_name, in_msa, add_to_msa )
269       seqs = in_msa.find_by_name_start( sequence_name, true )
270       if ( seqs.length < 1 )
271         error_msg = "sequence \"" + sequence_name + "\" not found in sequence file"
272         raise StandardError, error_msg
273       end
274       if ( seqs.length > 1 )
275         error_msg = "sequence \"" + sequence_name + "\" not unique in sequence file"
276         raise StandardError, error_msg
277       end
278       seq = in_msa.get_sequence( seqs[ 0 ] )
279       add_to_msa.add_sequence( seq )
280     end
281
282     def process_hmmscan_datas( hmmscan_datas,
283         in_msa,
284         add_position,
285         add_domain_number,
286         add_species,
287         out_msa,
288         out_msa_singles,
289         out_msa_pairs,
290         out_msa_isolated,
291         min_linker,
292         out_msa_single_domains_protein_seqs,
293         out_msa_close_pairs_protein_seqs,
294         out_msa_close_pairs_only_protein_seqs,
295         out_msa_isolated_protein_seqs,
296         out_msa_isolated_only_protein_seqs,
297         out_msa_isolated_and_close_pair_protein_seqs )
298
299       actual_out_of = hmmscan_datas.size
300       saw_close_pair = false
301       saw_isolated = false
302
303       seq_name = ""
304       prev_seq_name = nil
305
306       hmmscan_datas.each_with_index do |hmmscan_data, index|
307         if hmmscan_data.number < ( index + 1 )
308           error_msg = "hmmscan_data.number < ( index + 1 ) (this should not have happened)"
309           raise StandardError, error_msg
310         end
311
312         seq_name =  hmmscan_data.seq_name
313
314         extract_domain( seq_name,
315           index + 1,
316           actual_out_of,
317           hmmscan_data.env_from,
318           hmmscan_data.env_to,
319           in_msa,
320           out_msa,
321           add_position,
322           add_domain_number,
323           add_species )
324
325         if min_linker
326           if actual_out_of == 1
327             extract_domain( seq_name,
328               1,
329               1,
330               hmmscan_data.env_from,
331               hmmscan_data.env_to,
332               in_msa,
333               out_msa_singles,
334               add_position,
335               add_domain_number,
336               add_species )
337             if out_msa_single_domains_protein_seqs.find_by_name_start( seq_name, true ).length < 1
338               add_sequence( seq_name, in_msa, out_msa_single_domains_protein_seqs )
339             else
340               error_msg = "this should not have happened"
341               raise StandardError, error_msg
342             end
343
344           else
345             first = index == 0
346             last = index == hmmscan_datas.length - 1
347
348             if ( ( first && ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker) )  ||
349                  ( last && ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) ||
350                  ( !first && !last &&  ( ( hmmscan_datas[ index + 1 ].env_from - hmmscan_data.env_to ) > min_linker ) &&
351                    ( ( hmmscan_data.env_from - hmmscan_datas[ index - 1 ].env_to ) > min_linker ) ) )
352
353               extract_domain( seq_name,
354                 index + 1,
355                 actual_out_of,
356                 hmmscan_data.env_from,
357                 hmmscan_data.env_to,
358                 in_msa,
359                 out_msa_isolated,
360                 add_position,
361                 add_domain_number,
362                 add_species )
363               saw_isolated = true
364
365             elsif !first
366               extract_domain( seq_name,
367                 index.to_s  + "+" + ( index + 1 ).to_s,
368                 actual_out_of,
369                 hmmscan_datas[ index - 1 ].env_from,
370                 hmmscan_data.env_to,
371                 in_msa,
372                 out_msa_pairs,
373                 add_position,
374                 add_domain_number,
375                 add_species )
376               saw_close_pair = true
377             end
378           end
379         end
380         if prev_seq_name && prev_seq_name != seq_name
381           error_msg = "this should not have happened"
382           raise StandardError, error_msg
383         end
384         prev_seq_name = seq_name
385       end # each
386       if saw_isolated
387         if out_msa_isolated_protein_seqs.find_by_name_start( seq_name, true ).length < 1
388           add_sequence( seq_name, in_msa, out_msa_isolated_protein_seqs )
389         else
390           error_msg = "this should not have happened"
391           raise StandardError, error_msg
392         end
393       end
394       if saw_close_pair
395         if out_msa_close_pairs_protein_seqs.find_by_name_start( seq_name, true ).length < 1
396           add_sequence( seq_name, in_msa, out_msa_close_pairs_protein_seqs )
397         else
398           error_msg = "this should not have happened"
399           raise StandardError, error_msg
400         end
401       end
402       if saw_close_pair && saw_isolated
403         if out_msa_isolated_and_close_pair_protein_seqs.find_by_name_start( seq_name, true ).length < 1
404           add_sequence( seq_name, in_msa, out_msa_isolated_and_close_pair_protein_seqs )
405         else
406           error_msg = "this should not have happened"
407           raise StandardError, error_msg
408         end
409       elsif saw_close_pair
410         if out_msa_close_pairs_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
411           add_sequence( seq_name, in_msa, out_msa_close_pairs_only_protein_seqs )
412         else
413           error_msg = "this should not have happened"
414           raise StandardError, error_msg
415         end
416       elsif saw_isolated
417         if out_msa_isolated_only_protein_seqs.find_by_name_start( seq_name, true ).length < 1
418           add_sequence( seq_name, in_msa, out_msa_isolated_only_protein_seqs )
419         else
420           error_msg = "this should not have happened"
421           raise StandardError, error_msg
422         end
423       end
424     end # def process_hmmscan_data
425
426     def extract_domain( sequence,
427         number,
428         out_of,
429         seq_from,
430         seq_to,
431         in_msa,
432         out_msa,
433         add_position,
434         add_domain_number,
435         add_species )
436       if  number.is_a?( Fixnum ) && ( number < 1 || out_of < 1 || number > out_of )
437         error_msg = "impossible: number=" + number.to_s + ", out of=" + out_of.to_s
438         raise StandardError, error_msg
439       end
440       if  seq_from < 1 || seq_to < 1 || seq_from >= seq_to
441         error_msg = "impossible: seq-f=" + seq_from.to_s + ", seq-t=" + seq_to.to_s
442         raise StandardError, error_msg
443       end
444       seqs = in_msa.find_by_name_start( sequence, true )
445       if seqs.length < 1
446         error_msg = "sequence \"" + sequence + "\" not found in sequence file"
447         raise IOError, error_msg
448       end
449       if seqs.length > 1
450         error_msg = "sequence \"" + sequence + "\" not unique in sequence file"
451         raise IOError, error_msg
452       end
453       # hmmsearch is 1 based, wheres sequences are 0 bases in this package.
454       seq = in_msa.get_sequence( seqs[ 0 ] ).get_subsequence( seq_from - 1, seq_to - 1 )
455
456       orig_name = seq.get_name
457
458       seq.set_name( orig_name.split[ 0 ] )
459
460       if add_position
461         seq.set_name( seq.get_name + "_" + seq_from.to_s + "-" + seq_to.to_s )
462       end
463
464       if out_of != 1 && add_domain_number
465         seq.set_name( seq.get_name + "~" + number.to_s + "-" + out_of.to_s )
466       end
467
468       if add_species
469         a = orig_name.rindex "["
470         b = orig_name.rindex "]"
471         unless a && b
472           error_msg = "species not found in " + orig_name
473           raise StandardError, error_msg
474         end
475         species = orig_name[ a .. b ]
476         seq.set_name( seq.get_name + " " + species )
477       end
478       out_msa.add_sequence( seq )
479     end
480
481     def is_ignorable?( line )
482       return ( line !~ /[A-Za-z0-9-]/ || line =~/^#/ )
483     end
484
485   end # class HmmscanDomainExtractor
486
487   class HmmsearchData
488     def initialize( seq_name, number, out_of, env_from, env_to, i_e_value )
489       @seq_name = seq_name
490       @number = number
491       @out_of = out_of
492       @env_from = env_from
493       @env_to = env_to
494       @i_e_value = i_e_value
495     end
496     attr_reader :seq_name, :number, :out_of, :env_from, :env_to, :i_e_value
497   end
498
499 end # module Evoruby
500