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